autofill/sync/address/
incoming.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6use super::AddressPayload;
7use crate::db::addresses::{add_internal_address, update_internal_address};
8use crate::db::models::address::InternalAddress;
9use crate::db::schema::ADDRESS_COMMON_COLS;
10use crate::db::CounterUpdate;
11use crate::error::*;
12use crate::sync::address::name_utils::{join_name_parts, split_name, NameParts};
13use crate::sync::common::*;
14use crate::sync::{
15    IncomingBso, IncomingContent, IncomingEnvelope, IncomingKind, IncomingState, LocalRecordInfo,
16    ProcessIncomingRecordImpl, ServerTimestamp, SyncRecord,
17};
18use interrupt_support::Interruptee;
19use rusqlite::{named_params, Transaction};
20use sql_support::ConnExt;
21use sync_guid::Guid as SyncGuid;
22
23// When an incoming record lacks the `name` field but includes any `*_name` fields, we can
24// assume that the record originates from an older device.
25
26// If the record comes from an older device, we compare the `*_name` fields with those in
27// the corresponding local record. If the values of the `*_name`
28// fields differ, it indicates that the incoming record has updated these fields. If the
29// values are the same, we replace the name field of the incoming record with the local
30// name field to ensure the completeness of the name field when reconciling.
31//
32// Here is an example:
33// Assume the local record is {"name": "Mr. John Doe"}. If an updated incoming record
34// has {"given_name": "John", "family_name": "Doe"}, we will NOT join the `*_name` fields
35// and replace the local `name` field with "John Doe". This allows us to retain the complete
36// name - "Mr. John Doe".
37// However, if the updated incoming record has {"given_name": "Jane", "family_name": "Poe"},
38// we will rebuild it and replace the local `name` field with "Jane Poe".
39fn update_name(payload_content: &mut IncomingContent<AddressPayload>, local_name: String) {
40    // Check if the kind is IncomingKind::Content and get a mutable reference to internal_address
41    let internal_address =
42        if let IncomingKind::Content(internal_address) = &mut payload_content.kind {
43            internal_address
44        } else {
45            return;
46        };
47
48    let entry = &mut internal_address.entry;
49
50    // Return early if the name is not empty or `*-name`` parts are empty
51    if !entry.name.is_empty()
52        || (entry.given_name.is_empty()
53            && entry.additional_name.is_empty()
54            && entry.family_name.is_empty())
55    {
56        return;
57    }
58
59    // Split the local name into its parts
60    let NameParts {
61        given,
62        middle,
63        family,
64    } = split_name(&local_name);
65
66    // Check if the local name matches the entry names
67    let is_local_name_matching =
68        entry.given_name == given && entry.additional_name == middle && entry.family_name == family;
69
70    // Update the name based on whether the local name matches
71    entry.name = if is_local_name_matching {
72        local_name
73    } else {
74        join_name_parts(&NameParts {
75            given: entry.given_name.clone(),
76            middle: entry.additional_name.clone(),
77            family: entry.family_name.clone(),
78        })
79    };
80}
81
82fn create_incoming_bso(id: SyncGuid, raw: String) -> IncomingContent<AddressPayload> {
83    let bso = IncomingBso {
84        envelope: IncomingEnvelope {
85            id,
86            modified: ServerTimestamp::default(),
87            sortindex: None,
88            ttl: None,
89        },
90        payload: raw,
91    };
92    bso.into_content::<AddressPayload>()
93}
94
95fn bso_to_incoming(
96    payload_content: IncomingContent<AddressPayload>,
97) -> Result<IncomingContent<InternalAddress>> {
98    Ok(match payload_content.kind {
99        IncomingKind::Content(content) => IncomingContent {
100            envelope: payload_content.envelope,
101            kind: IncomingKind::Content(InternalAddress::from_payload(content)?),
102        },
103        IncomingKind::Tombstone => IncomingContent {
104            envelope: payload_content.envelope,
105            kind: IncomingKind::Tombstone,
106        },
107        IncomingKind::Malformed => IncomingContent {
108            envelope: payload_content.envelope,
109            kind: IncomingKind::Malformed,
110        },
111    })
112}
113
114// Takes a raw payload, as stored in our database, and returns an InternalAddress
115// or a tombstone. Addresses store the raw payload as cleartext json.
116fn raw_payload_to_incoming(id: SyncGuid, raw: String) -> Result<IncomingContent<InternalAddress>> {
117    let payload_content = create_incoming_bso(id, raw);
118
119    Ok(match payload_content.kind {
120        IncomingKind::Content(content) => IncomingContent {
121            envelope: payload_content.envelope,
122            kind: IncomingKind::Content(InternalAddress::from_payload(content)?),
123        },
124        IncomingKind::Tombstone => IncomingContent {
125            envelope: payload_content.envelope,
126            kind: IncomingKind::Tombstone,
127        },
128        IncomingKind::Malformed => IncomingContent {
129            envelope: payload_content.envelope,
130            kind: IncomingKind::Malformed,
131        },
132    })
133}
134
135pub(super) struct IncomingAddressesImpl {}
136
137impl ProcessIncomingRecordImpl for IncomingAddressesImpl {
138    type Record = InternalAddress;
139
140    /// The first step in the "apply incoming" process - stage the records
141    fn stage_incoming(
142        &self,
143        tx: &Transaction<'_>,
144        incoming: Vec<IncomingBso>,
145        signal: &dyn Interruptee,
146    ) -> Result<()> {
147        let to_stage = incoming
148            .into_iter()
149            // We persist the entire payload as cleartext - which it already is!
150            .map(|bso| (bso.envelope.id, bso.payload, bso.envelope.modified))
151            .collect();
152        common_stage_incoming_records(tx, "addresses_sync_staging", to_stage, signal)
153    }
154
155    fn finish_incoming(&self, tx: &Transaction<'_>) -> Result<()> {
156        common_mirror_staged_records(tx, "addresses_sync_staging", "addresses_mirror")
157    }
158
159    /// The second step in the "apply incoming" process for syncing autofill address records.
160    /// Incoming items are retrieved from the temp tables, deserialized, and
161    /// assigned `IncomingState` values.
162    fn fetch_incoming_states(
163        &self,
164        tx: &Transaction<'_>,
165    ) -> Result<Vec<IncomingState<Self::Record>>> {
166        let sql = "
167        SELECT
168            s.guid as guid,
169            l.guid as l_guid,
170            t.guid as t_guid,
171            s.payload as s_payload,
172            m.payload as m_payload,
173            l.name,
174            l.organization,
175            l.street_address,
176            l.address_level3,
177            l.address_level2,
178            l.address_level1,
179            l.postal_code,
180            l.country,
181            l.tel,
182            l.email,
183            l.time_created,
184            l.time_last_used,
185            l.time_last_modified,
186            l.times_used,
187            l.sync_change_counter
188        FROM temp.addresses_sync_staging s
189        LEFT JOIN addresses_mirror m ON s.guid = m.guid
190        LEFT JOIN addresses_data l ON s.guid = l.guid
191        LEFT JOIN addresses_tombstones t ON s.guid = t.guid";
192
193        tx.query_rows_and_then(sql, [], |row| -> Result<IncomingState<Self::Record>> {
194            // the 'guid' and 's_payload' rows must be non-null.
195            let guid: SyncGuid = row.get("guid")?;
196
197            // We update the 'name' field using the update_name function.
198            // We utilize create_incoming_bso and bso_to_incoming functions
199            // instead of payload_to_incoming. This is done to avoid directly passing
200            // row.get("name") to payload_to_incoming, which would result in having to pass
201            // None parameters in a few places.
202            let mut payload_content = create_incoming_bso(guid.clone(), row.get("s_payload")?);
203            update_name(
204                &mut payload_content,
205                row.get("name").unwrap_or("".to_string()),
206            );
207            let incoming = bso_to_incoming(payload_content)?;
208
209            Ok(IncomingState {
210                incoming,
211                local: match row.get_unwrap::<_, Option<String>>("l_guid") {
212                    Some(l_guid) => {
213                        assert_eq!(l_guid, guid);
214                        // local record exists, check the state.
215                        let record = InternalAddress::from_row(row)?;
216                        let has_changes = record.metadata().sync_change_counter != 0;
217                        if has_changes {
218                            LocalRecordInfo::Modified { record }
219                        } else {
220                            LocalRecordInfo::Unmodified { record }
221                        }
222                    }
223                    None => {
224                        // no local record - maybe a tombstone?
225                        match row.get::<_, Option<String>>("t_guid")? {
226                            Some(t_guid) => {
227                                assert_eq!(guid, t_guid);
228                                LocalRecordInfo::Tombstone { guid: guid.clone() }
229                            }
230                            None => LocalRecordInfo::Missing,
231                        }
232                    }
233                },
234                mirror: {
235                    match row.get::<_, Option<String>>("m_payload")? {
236                        Some(m_payload) => {
237                            // a tombstone in the mirror can be treated as though it's missing.
238                            raw_payload_to_incoming(guid, m_payload)?.content()
239                        }
240                        None => None,
241                    }
242                },
243            })
244        })
245    }
246
247    /// Returns a local record that has the same values as the given incoming record (with the exception
248    /// of the `guid` values which should differ) that will be used as a local duplicate record for
249    /// syncing.
250    fn get_local_dupe(
251        &self,
252        tx: &Transaction<'_>,
253        incoming: &Self::Record,
254    ) -> Result<Option<Self::Record>> {
255        let sql = format!("
256            SELECT
257                {common_cols},
258                sync_change_counter
259            FROM addresses_data
260            WHERE
261                -- `guid <> :guid` is a pre-condition for this being called, but...
262                guid <> :guid
263                -- only non-synced records are candidates, which means can't already be in the mirror.
264                AND guid NOT IN (
265                    SELECT guid
266                    FROM addresses_mirror
267                )
268                -- and sql can check the field values.
269                AND name == :name
270                AND organization == :organization
271                AND street_address == :street_address
272                AND address_level3 == :address_level3
273                AND address_level2 == :address_level2
274                AND address_level1 == :address_level1
275                AND postal_code == :postal_code
276                AND country == :country
277                AND tel == :tel
278                AND email == :email", common_cols = ADDRESS_COMMON_COLS);
279
280        let params = named_params! {
281            ":guid": incoming.guid,
282            ":name": incoming.name,
283            ":organization": incoming.organization,
284            ":street_address": incoming.street_address,
285            ":address_level3": incoming.address_level3,
286            ":address_level2": incoming.address_level2,
287            ":address_level1": incoming.address_level1,
288            ":postal_code": incoming.postal_code,
289            ":country": incoming.country,
290            ":tel": incoming.tel,
291            ":email": incoming.email,
292        };
293
294        let result = tx.query_row(&sql, params, |row| {
295            Ok(Self::Record::from_row(row).expect("wtf? '?' doesn't work :("))
296        });
297
298        match result {
299            Ok(r) => Ok(Some(r)),
300            Err(e) => match e {
301                rusqlite::Error::QueryReturnedNoRows => Ok(None),
302                _ => Err(Error::SqlError(e)),
303            },
304        }
305    }
306
307    fn update_local_record(
308        &self,
309        tx: &Transaction<'_>,
310        new_record: Self::Record,
311        flag_as_changed: bool,
312    ) -> Result<()> {
313        update_internal_address(
314            tx,
315            &new_record,
316            if flag_as_changed {
317                CounterUpdate::Increment
318            } else {
319                CounterUpdate::Leave
320            },
321        )?;
322        Ok(())
323    }
324
325    fn insert_local_record(&self, tx: &Transaction<'_>, new_record: Self::Record) -> Result<()> {
326        add_internal_address(tx, &new_record)?;
327        Ok(())
328    }
329
330    /// Changes the guid of the local record for the given `old_guid` to the given `new_guid` used
331    /// for the `HasLocalDupe` incoming state, and mark the item as dirty.
332    /// We also update the mirror record if it exists in forking scenarios
333    fn change_record_guid(
334        &self,
335        tx: &Transaction<'_>,
336        old_guid: &SyncGuid,
337        new_guid: &SyncGuid,
338    ) -> Result<()> {
339        common_change_guid(tx, "addresses_data", "addresses_mirror", old_guid, new_guid)
340    }
341
342    fn remove_record(&self, tx: &Transaction<'_>, guid: &SyncGuid) -> Result<()> {
343        common_remove_record(tx, "addresses_data", guid)
344    }
345
346    fn remove_tombstone(&self, tx: &Transaction<'_>, guid: &SyncGuid) -> Result<()> {
347        common_remove_record(tx, "addresses_tombstones", guid)
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::super::super::test::new_syncable_mem_db;
354    use super::*;
355    use crate::db::addresses::get_address;
356    use crate::sync::common::tests::*;
357
358    use error_support::info;
359    use interrupt_support::NeverInterrupts;
360    use serde_json::{json, Map, Value};
361    use sql_support::ConnExt;
362
363    impl InternalAddress {
364        fn into_test_incoming_bso(self) -> IncomingBso {
365            IncomingBso::from_test_content(self.into_payload().expect("is json"))
366        }
367    }
368
369    lazy_static::lazy_static! {
370        static ref TEST_JSON_RECORDS: Map<String, Value> = {
371            // NOTE: the JSON here is the same as stored on the sync server -
372            // the superfluous `entry` is unfortunate but from desktop.
373            // JSON from the server is kebab-style, EXCEPT the times{X} fields
374            // see PayloadEntry struct
375            let val = json! {{
376                "A" : {
377                    "id": expand_test_guid('A'),
378                    "entry": {
379                        "name": "john doe",
380                        "given-name": "john",
381                        "family-name": "doe",
382                        "street-address": "1300 Broadway",
383                        "address-level2": "New York, NY",
384                        "country": "United States",
385                        "version": 1,
386                    }
387                },
388                "C" : {
389                    "id": expand_test_guid('C'),
390                    "entry": {
391                        "name": "jane doe",
392                        "given-name": "jane",
393                        "family-name": "doe",
394                        "street-address": "3050 South La Brea Ave",
395                        "address-level2": "Los Angeles, CA",
396                        "country": "United States",
397                        "timeCreated": 0,
398                        "timeLastUsed": 0,
399                        "timeLastModified": 0,
400                        "timesUsed": 0,
401                        "version": 1,
402                    }
403                },
404                "D" : {
405                    "id": expand_test_guid('D'),
406                    "entry": {
407                        "name": "test1 test2",
408                        "given-name": "test1",
409                        "family-name": "test2",
410                        "street-address": "85 Pike St",
411                        "address-level2": "Seattle, WA",
412                        "country": "United States",
413                        "foo": "bar",
414                        "baz": "qux",
415                        "version": 1,
416                    }
417                }
418            }};
419            val.as_object().expect("literal is an object").clone()
420        };
421    }
422
423    fn test_json_record(guid_prefix: char) -> Value {
424        TEST_JSON_RECORDS
425            .get(&guid_prefix.to_string())
426            .expect("should exist")
427            .clone()
428    }
429
430    fn test_record(guid_prefix: char) -> InternalAddress {
431        let json = test_json_record(guid_prefix);
432        let address_payload = serde_json::from_value(json).unwrap();
433        InternalAddress::from_payload(address_payload).expect("should be valid")
434    }
435
436    #[test]
437    fn test_stage_incoming() -> Result<()> {
438        error_support::init_for_tests();
439        let mut db = new_syncable_mem_db();
440        struct TestCase {
441            incoming_records: Vec<Value>,
442            mirror_records: Vec<Value>,
443            expected_record_count: usize,
444            expected_tombstone_count: usize,
445        }
446
447        let test_cases = vec![
448            TestCase {
449                incoming_records: vec![test_json_record('A')],
450                mirror_records: vec![],
451                expected_record_count: 1,
452                expected_tombstone_count: 0,
453            },
454            TestCase {
455                incoming_records: vec![test_json_tombstone('A')],
456                mirror_records: vec![],
457                expected_record_count: 0,
458                expected_tombstone_count: 1,
459            },
460            TestCase {
461                incoming_records: vec![
462                    test_json_record('A'),
463                    test_json_record('C'),
464                    test_json_tombstone('B'),
465                ],
466                mirror_records: vec![],
467                expected_record_count: 2,
468                expected_tombstone_count: 1,
469            },
470            // incoming tombstone with existing tombstone in the mirror
471            TestCase {
472                incoming_records: vec![test_json_tombstone('B')],
473                mirror_records: vec![test_json_tombstone('B')],
474                expected_record_count: 0,
475                expected_tombstone_count: 1,
476            },
477        ];
478
479        for tc in test_cases {
480            info!("starting new testcase");
481            let tx = db.transaction()?;
482
483            // Add required items to the mirrors.
484            let mirror_sql = "INSERT OR REPLACE INTO addresses_mirror (guid, payload)
485                              VALUES (:guid, :payload)";
486            for payload in tc.mirror_records {
487                tx.execute(
488                    mirror_sql,
489                    rusqlite::named_params! {
490                        ":guid": payload["id"].as_str().unwrap(),
491                        ":payload": payload.to_string(),
492                    },
493                )
494                .expect("should insert mirror record");
495            }
496
497            let ri = IncomingAddressesImpl {};
498            ri.stage_incoming(
499                &tx,
500                array_to_incoming(tc.incoming_records),
501                &NeverInterrupts,
502            )?;
503
504            let records = tx.conn().query_rows_and_then(
505                "SELECT * FROM temp.addresses_sync_staging;",
506                [],
507                |row| -> Result<IncomingContent<InternalAddress>> {
508                    let guid: SyncGuid = row.get_unwrap("guid");
509                    let payload: String = row.get_unwrap("payload");
510                    raw_payload_to_incoming(guid, payload)
511                },
512            )?;
513
514            let record_count = records
515                .iter()
516                .filter(|p| !matches!(p.kind, IncomingKind::Tombstone))
517                .count();
518            let tombstone_count = records.len() - record_count;
519
520            assert_eq!(record_count, tc.expected_record_count);
521            assert_eq!(tombstone_count, tc.expected_tombstone_count);
522
523            ri.fetch_incoming_states(&tx)?;
524
525            tx.execute("DELETE FROM temp.addresses_sync_staging;", [])?;
526        }
527        Ok(())
528    }
529
530    #[test]
531    fn test_change_record_guid() -> Result<()> {
532        let mut db = new_syncable_mem_db();
533        let tx = db.transaction()?;
534        let ri = IncomingAddressesImpl {};
535
536        ri.insert_local_record(&tx, test_record('C'))?;
537
538        ri.change_record_guid(
539            &tx,
540            &SyncGuid::new(&expand_test_guid('C')),
541            &SyncGuid::new(&expand_test_guid('B')),
542        )?;
543        tx.commit()?;
544        assert!(get_address(&db.writer, &expand_test_guid('C').into()).is_err());
545        assert!(get_address(&db.writer, &expand_test_guid('B').into()).is_ok());
546        Ok(())
547    }
548
549    #[test]
550    fn test_get_incoming() {
551        let mut db = new_syncable_mem_db();
552        let tx = db.transaction().expect("should get tx");
553        let ai = IncomingAddressesImpl {};
554        let record = test_record('C');
555        let bso = record.clone().into_test_incoming_bso();
556        do_test_incoming_same(&ai, &tx, record, bso);
557    }
558
559    #[test]
560    fn test_get_incoming_unknown_fields() {
561        let json = test_json_record('D');
562        let address_payload = serde_json::from_value::<AddressPayload>(json).unwrap();
563        // The incoming payload should've correctly deserialized any unknown_fields into a Map<String,Value>
564        assert_eq!(address_payload.entry.unknown_fields.len(), 2);
565        assert_eq!(
566            address_payload
567                .entry
568                .unknown_fields
569                .get("foo")
570                .unwrap()
571                .as_str()
572                .unwrap(),
573            "bar"
574        );
575    }
576
577    #[test]
578    fn test_incoming_tombstone() {
579        let mut db = new_syncable_mem_db();
580        let tx = db.transaction().expect("should get tx");
581        let ai = IncomingAddressesImpl {};
582        do_test_incoming_tombstone(&ai, &tx, test_record('C'));
583    }
584
585    #[test]
586    fn test_staged_to_mirror() {
587        let mut db = new_syncable_mem_db();
588        let tx = db.transaction().expect("should get tx");
589        let ai = IncomingAddressesImpl {};
590        let record = test_record('C');
591        let bso = record.clone().into_test_incoming_bso();
592        do_test_staged_to_mirror(&ai, &tx, record, bso, "addresses_mirror");
593    }
594}