1const REMOTE_COMMAND_TTL_MS: u64 = 2 * 24 * 60 * 60 * 1000; use crate::error::*;
11use crate::schema;
12use crate::sync::TabsRecord;
13use crate::DeviceType;
14use crate::{PendingCommand, RemoteCommand, Timestamp};
15use error_support::{error, info, trace, warn};
16use rusqlite::{
17 types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef},
18 Connection, OpenFlags,
19};
20use sql_support::open_database::{self, open_database_with_flags};
21use sql_support::ConnExt;
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25use sync15::{RemoteClient, ServerTimestamp};
26pub type TabsDeviceType = crate::DeviceType;
27
28pub(crate) const TABS_CLIENT_TTL: u32 = 15_552_000; const FAR_FUTURE: i64 = 4_102_405_200_000; #[derive(Clone, Debug, Default, uniffi::Record)]
32#[cfg_attr(test, derive(PartialEq, Eq))] pub struct RemoteTabRecord {
34 pub title: String,
35 pub url_history: Vec<String>,
36 pub icon: Option<String>,
37 pub last_used: i64, #[uniffi(default = false)]
39 pub inactive: bool,
40 #[uniffi(default = false)]
41 pub pinned: bool,
42 #[uniffi(default = 0)]
44 pub index: u32,
45 #[uniffi(default = "")]
46 pub window_id: String,
47 #[uniffi(default = "")]
48 pub tab_group_id: String,
49}
50pub type RemoteTab = RemoteTabRecord;
51
52#[derive(Clone, Debug, uniffi::Record)]
53pub struct ClientRemoteTabs {
54 pub client_id: String,
56 pub client_name: String,
57 pub device_type: DeviceType,
58 pub last_modified: i64,
60 pub remote_tabs: Vec<RemoteTab>,
61 pub tab_groups: HashMap<String, TabGroup>,
62 pub windows: HashMap<String, Window>,
63}
64
65#[derive(uniffi::Enum, Clone, Debug, Default)]
66#[repr(u8)]
67pub enum WindowType {
68 #[default]
69 Normal = 0,
70}
71
72impl From<u8> for WindowType {
73 fn from(value: u8) -> Self {
74 match value {
75 0 => WindowType::Normal,
76 _ => {
77 warn!("Unknown window type {}, defaulting to Normal", value);
78 WindowType::Normal
79 }
80 }
81 }
82}
83
84#[derive(uniffi::Record, Debug, Clone)]
85#[cfg_attr(test, derive(Default))]
86pub struct Window {
87 pub id: String,
88 pub last_used: Timestamp,
89 pub index: u32,
90 pub window_type: WindowType,
91}
92
93#[derive(uniffi::Record, Debug, Clone)]
95#[cfg_attr(test, derive(Default))]
96pub struct TabGroup {
97 pub id: String,
98 pub name: String,
99 pub color: String,
100 pub collapsed: bool,
101}
102
103#[derive(uniffi::Record, Debug, Clone, Default)]
105pub struct LocalTabsInfo {
106 pub tabs: Vec<RemoteTab>,
107 pub tab_groups: HashMap<String, TabGroup>,
108 pub windows: HashMap<String, Window>,
109}
110
111pub(crate) enum DbConnection {
112 Created,
113 Open(Connection),
114 Closed,
115}
116
117pub struct TabsStorage {
130 pub(crate) local_tabs: RefCell<Option<LocalTabsInfo>>,
131 db_path: PathBuf,
132 db_connection: DbConnection,
133}
134
135impl TabsStorage {
136 pub fn new(db_path: impl AsRef<Path>) -> Self {
137 Self {
138 local_tabs: RefCell::default(),
139 db_path: db_path.as_ref().to_path_buf(),
140 db_connection: DbConnection::Created,
141 }
142 }
143
144 pub fn close(&mut self) {
145 if let DbConnection::Open(conn) =
146 std::mem::replace(&mut self.db_connection, DbConnection::Closed)
147 {
148 if let Err(err) = conn.close() {
149 error!("Failed to close the connection: {:?}", err);
151 }
152 }
153 }
154
155 pub fn new_with_mem_path(db_path: &str) -> Self {
158 let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
159 Self::new(name)
160 }
161
162 pub fn open_if_exists(&mut self) -> Result<Option<&Connection>> {
164 match self.db_connection {
165 DbConnection::Open(ref conn) => return Ok(Some(conn)),
166 DbConnection::Closed => return Ok(None),
167 DbConnection::Created => {}
168 }
169 let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
170 | OpenFlags::SQLITE_OPEN_URI
171 | OpenFlags::SQLITE_OPEN_READ_WRITE;
172 match open_database_with_flags(
173 self.db_path.clone(),
174 flags,
175 &crate::schema::TabsMigrationLogic,
176 ) {
177 Ok(conn) => {
178 info!("tabs storage is opening an existing database");
179 self.db_connection = DbConnection::Open(conn);
180 match self.db_connection {
181 DbConnection::Open(ref conn) => Ok(Some(conn)),
182 _ => unreachable!("impossible value"),
183 }
184 }
185 Err(open_database::Error::SqlError(rusqlite::Error::SqliteFailure(code, _)))
186 if code.code == rusqlite::ErrorCode::CannotOpen =>
187 {
188 info!("tabs storage could not open an existing database and hasn't been asked to create one");
189 Ok(None)
190 }
191 Err(e) => Err(e.into()),
192 }
193 }
194
195 pub fn open_or_create(&mut self) -> Result<&Connection> {
197 match self.db_connection {
198 DbConnection::Open(ref conn) => return Ok(conn),
199 DbConnection::Closed => return Err(Error::UnexpectedConnectionState),
200 DbConnection::Created => {}
201 }
202 let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
203 | OpenFlags::SQLITE_OPEN_URI
204 | OpenFlags::SQLITE_OPEN_READ_WRITE
205 | OpenFlags::SQLITE_OPEN_CREATE;
206 let conn = open_database_with_flags(
207 self.db_path.clone(),
208 flags,
209 &crate::schema::TabsMigrationLogic,
210 )?;
211 info!("tabs storage is creating a database connection");
212 self.db_connection = DbConnection::Open(conn);
213 match self.db_connection {
214 DbConnection::Open(ref conn) => Ok(conn),
215 _ => unreachable!("We just set to Open, this should be impossible."),
216 }
217 }
218
219 pub fn update_local_state(&mut self, local_state: LocalTabsInfo) {
220 let num_tabs = local_state.tabs.len();
221 self.local_tabs.borrow_mut().replace(local_state);
222 info!("update_local_state has {num_tabs} tab entries");
223 }
224
225 pub fn get_remote_tabs(&mut self) -> Option<Vec<ClientRemoteTabs>> {
226 let conn = match self.open_if_exists() {
227 Err(e) => {
228 error_support::report_error!(
229 "tabs-read-remote",
230 "Failed to read remote tabs: {}",
231 e
232 );
233 return None;
234 }
235 Ok(None) => return None,
236 Ok(Some(conn)) => conn,
237 };
238
239 let records: Vec<(TabsRecord, ServerTimestamp)> = match conn.query_rows_and_then_cached(
240 "SELECT record, last_modified FROM tabs",
241 [],
242 |row| -> Result<_> {
243 Ok((
244 serde_json::from_str(&row.get::<_, String>(0)?)?,
245 ServerTimestamp(row.get::<_, i64>(1)?),
246 ))
247 },
248 ) {
249 Ok(records) => records,
250 Err(e) => {
251 error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
252 return None;
253 }
254 };
255 let mut crts: Vec<ClientRemoteTabs> = Vec::new();
256 let remote_clients: HashMap<String, RemoteClient> =
257 match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY) {
258 Err(e) => {
259 error_support::report_error!(
260 "tabs-read-remote",
261 "Failed to get remote clients: {}",
262 e
263 );
264 return None;
265 }
266 Ok(None) => HashMap::default(),
269 Ok(Some(json)) => serde_json::from_str(&json).unwrap(),
270 };
271 for (record, last_modified) in records {
272 let id = record.id.clone();
273 if let Some(remote_client) = remote_clients.get(&id) {
274 crts.push(ClientRemoteTabs::from_record(
275 remote_client
276 .fxa_device_id
277 .as_ref()
278 .unwrap_or(&id)
279 .to_owned(),
280 last_modified,
281 remote_client,
282 record,
283 ));
284 } else {
285 warn!("Dropping tabs from an unknown client: {id}");
287 };
288 }
289 let filtered_crts = self.filter_pending_remote_tabs(crts);
293 Some(filtered_crts)
294 }
295
296 fn filter_pending_remote_tabs(&mut self, crts: Vec<ClientRemoteTabs>) -> Vec<ClientRemoteTabs> {
297 let conn = match self.open_if_exists() {
298 Err(e) => {
299 error_support::report_error!(
300 "tabs-read-remote",
301 "Failed to read remote tabs: {}",
302 e
303 );
304 return crts;
305 }
306 Ok(None) => return crts,
307 Ok(Some(conn)) => conn,
308 };
309 let pending_tabs_result: Result<Vec<(String, String)>> = conn.query_rows_and_then_cached(
310 "SELECT device_id, url
311 FROM remote_tab_commands
312 WHERE command = :command_close_tab",
313 rusqlite::named_params! { ":command_close_tab": CommandKind::CloseTab },
314 |row| {
315 Ok((
316 row.get::<_, String>(0)?, row.get::<_, String>(1)?, ))
319 },
320 );
321 let pending_closures = match pending_tabs_result {
323 Ok(pending_closures) => pending_closures.into_iter().fold(
324 HashMap::new(),
325 |mut acc: HashMap<String, Vec<String>>, (device_id, url)| {
326 acc.entry(device_id).or_default().push(url);
327 acc
328 },
329 ),
330 Err(e) => {
331 error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
332 return crts;
333 }
334 };
335 let filtered_crts: Vec<ClientRemoteTabs> = crts
339 .into_iter()
340 .map(|mut crt| {
341 crt.remote_tabs.retain(|tab| {
342 !pending_closures
345 .get(&crt.client_id)
346 .is_some_and(|urls| urls.contains(&tab.url_history[0]))
347 });
348 crt
349 })
350 .collect();
351 filtered_crts
353 }
354
355 pub fn remove_stale_clients(&mut self) -> Result<()> {
360 let last_sync = self.get_meta::<i64>(schema::LAST_SYNC_META_KEY)?;
361 if let Some(conn) = self.open_if_exists()? {
362 if let Some(last_sync) = last_sync {
363 let client_ttl_ms = (TABS_CLIENT_TTL as i64) * 1000;
364 if last_sync - client_ttl_ms >= 0 && last_sync != (FAR_FUTURE * 1000) {
369 let tx = conn.unchecked_transaction()?;
370 let num_removed = tx.execute_cached(
371 "DELETE FROM tabs WHERE last_modified <= :last_sync - :ttl",
372 rusqlite::named_params! {
373 ":last_sync": last_sync,
374 ":ttl": client_ttl_ms,
375 },
376 )?;
377 info!(
378 "removed {} stale clients (threshold was {})",
379 num_removed,
380 last_sync - client_ttl_ms
381 );
382 tx.commit()?;
383 }
384 }
385 }
386 Ok(())
387 }
388
389 pub(crate) fn replace_remote_tabs(
390 &mut self,
391 new_remote_tabs: &Vec<(TabsRecord, ServerTimestamp)>,
394 ) -> Result<()> {
395 let connection = self.open_or_create()?;
396 let tx = connection.unchecked_transaction()?;
397
398 for remote_tab in new_remote_tabs {
401 let record = &remote_tab.0;
402 let last_modified = remote_tab.1;
403 info!(
404 "inserting tab for device {}, last modified at {}",
405 record.id,
406 last_modified.as_millis()
407 );
408 tx.execute_cached(
409 "INSERT OR REPLACE INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
410 rusqlite::named_params! {
411 ":guid": &record.id,
412 ":record": serde_json::to_string(&record).expect("tabs don't fail to serialize"),
413 ":last_modified": last_modified.as_millis()
414 },
415 )?;
416 }
417 tx.commit()?;
418 Ok(())
419 }
420
421 pub(crate) fn wipe_remote_tabs(&mut self) -> Result<()> {
422 if let Some(db) = self.open_if_exists()? {
423 db.execute_batch("DELETE FROM tabs")?;
424 }
425 Ok(())
426 }
427
428 pub(crate) fn wipe_local_tabs(&self) {
429 self.local_tabs.replace(None);
430 }
431
432 pub(crate) fn put_meta(&mut self, key: &str, value: &dyn ToSql) -> Result<()> {
433 let db = self.open_or_create()?;
434 db.execute_cached(
435 "REPLACE INTO moz_meta (key, value) VALUES (:key, :value)",
436 &[(":key", &key as &dyn ToSql), (":value", value)],
437 )?;
438 Ok(())
439 }
440
441 pub(crate) fn get_meta<T: FromSql>(&mut self, key: &str) -> Result<Option<T>> {
442 match self.open_if_exists() {
443 Ok(Some(db)) => {
444 let res = db.try_query_one(
445 "SELECT value FROM moz_meta WHERE key = :key",
446 &[(":key", &key)],
447 true,
448 )?;
449 Ok(res)
450 }
451 Err(e) => Err(e),
452 Ok(None) => Ok(None),
453 }
454 }
455
456 pub(crate) fn delete_meta(&mut self, key: &str) -> Result<()> {
457 if let Some(db) = self.open_if_exists()? {
458 db.execute_cached("DELETE FROM moz_meta WHERE key = :key", &[(":key", &key)])?;
459 }
460 Ok(())
461 }
462}
463
464impl TabsStorage {
467 pub fn add_remote_tab_command(
471 &mut self,
472 device_id: &str,
473 command: &RemoteCommand,
474 ) -> Result<bool> {
475 self.add_remote_tab_command_at(device_id, command, Timestamp::now())
476 }
477
478 pub fn add_remote_tab_command_at(
479 &mut self,
480 device_id: &str,
481 command: &RemoteCommand,
482 time_requested: Timestamp,
483 ) -> Result<bool> {
484 let connection = self.open_or_create()?;
485 let RemoteCommand::CloseTab { url } = command;
486 info!("Adding remote command for {device_id} at {time_requested}");
487 trace!("command is {command:?}");
488 let tx = connection.unchecked_transaction()?;
490 let changes = tx.execute_cached(
491 "INSERT OR IGNORE INTO remote_tab_commands
492 (device_id, command, url, time_requested, time_sent)
493 VALUES (:device_id, :command, :url, :time_requested, null)",
494 rusqlite::named_params! {
495 ":device_id": &device_id,
496 ":url": url,
497 ":time_requested": time_requested,
498 ":command": command.as_ref(),
499 },
500 )?;
501 tx.commit()?;
502 Ok(changes != 0)
503 }
504
505 pub fn remove_remote_tab_command(
506 &mut self,
507 device_id: &str,
508 command: &RemoteCommand,
509 ) -> Result<bool> {
510 let connection = self.open_or_create()?;
511 let RemoteCommand::CloseTab { url } = command;
512 info!("removing remote tab close details: client={device_id}");
513 let tx = connection.unchecked_transaction()?;
514 let changes = tx.execute_cached(
515 "DELETE FROM remote_tab_commands
516 WHERE device_id = :device_id AND command = :command AND url = :url;",
517 rusqlite::named_params! {
518 ":device_id": &device_id,
519 ":url": url,
520 ":command": command.as_ref(),
521 },
522 )?;
523 tx.commit()?;
524 Ok(changes != 0)
525 }
526
527 pub fn get_unsent_commands(&mut self) -> Result<Vec<PendingCommand>> {
528 self.do_get_pending_commands("WHERE time_sent IS NULL")
529 }
530
531 fn do_get_pending_commands(&mut self, where_clause: &str) -> Result<Vec<PendingCommand>> {
532 let Some(conn) = self.open_if_exists()? else {
533 return Ok(Vec::new());
534 };
535 let result = conn.query_rows_and_then_cached(
536 &format!(
537 "SELECT device_id, command, url, time_requested, time_sent
538 FROM remote_tab_commands
539 {where_clause}
540 ORDER BY time_requested
541 LIMIT 1000 -- sue me!"
542 ),
543 [],
544 |row| -> Result<_> {
545 let command = match row.get::<_, CommandKind>(1) {
547 Ok(c) => c,
548 Err(e) => {
549 error!("do_get_pending_commands: ignoring error fetching command: {e:?}");
550 return Ok(None);
551 }
552 };
553 Ok(Some(match command {
554 CommandKind::CloseTab => PendingCommand {
555 device_id: row.get::<_, String>(0)?,
556 command: RemoteCommand::CloseTab {
557 url: row.get::<_, String>(2)?,
558 },
559 time_requested: row.get::<_, Timestamp>(3)?,
560 time_sent: row.get::<_, Option<Timestamp>>(4)?,
561 },
562 }))
563 },
564 );
565 Ok(match result {
566 Ok(records) => records.into_iter().flatten().collect(),
567 Err(e) => {
568 error_support::report_error!("tabs-get_unsent", "Failed to read database: {}", e);
569 Vec::new()
570 }
571 })
572 }
573
574 pub fn set_pending_command_sent(&mut self, command: &PendingCommand) -> Result<bool> {
575 let connection = self.open_or_create()?;
576 let RemoteCommand::CloseTab { url } = &command.command;
577 info!("setting remote tab sent: client={}", command.device_id);
578 trace!("command: {command:?}");
579 let tx = connection.unchecked_transaction()?;
580 let ts = Timestamp::now();
581 let changes = tx.execute_cached(
582 "UPDATE remote_tab_commands
583 SET time_sent = :ts
584 WHERE device_id = :device_id AND command = :command AND url = :url;",
585 rusqlite::named_params! {
586 ":command": command.command.as_ref(),
587 ":device_id": &command.device_id,
588 ":url": url,
589 ":ts": &ts,
590 },
591 )?;
592 tx.commit()?;
593 Ok(changes != 0)
594 }
595
596 pub fn remove_old_pending_closures(
599 &mut self,
600 new_remote_tabs: &[(TabsRecord, ServerTimestamp)],
603 ) -> Result<()> {
604 let remote_clients: HashMap<String, RemoteClient> = {
607 match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY)? {
608 None => HashMap::default(),
609 Some(json) => serde_json::from_str(&json).unwrap(),
610 }
611 };
612
613 let conn = self.open_or_create()?;
614 let tx = conn.unchecked_transaction()?;
615
616 conn.execute(
618 "CREATE TEMP TABLE if not exists new_remote_tabs (device_id TEXT, url TEXT)",
619 [],
620 )?;
621 conn.execute("DELETE FROM new_remote_tabs", [])?; for (record, _) in new_remote_tabs.iter() {
624 let fxa_id = remote_clients
625 .get(&record.id)
626 .and_then(|r| r.fxa_device_id.as_ref())
627 .unwrap_or(&record.id);
628 for tab in &record.tabs {
629 if let Some(url) = tab.url_history.first() {
630 conn.execute(
631 "INSERT INTO new_remote_tabs (device_id, url) VALUES (?, ?)",
632 rusqlite::params![fxa_id, url],
633 )?;
634 }
635 }
636 }
637
638 let delete_sql = "
640 DELETE FROM remote_tab_commands
641 WHERE
642 (device_id IN (SELECT device_id from new_remote_tabs))
643 AND
644 (
645 url NOT IN (
646 SELECT url from new_remote_tabs
647 WHERE new_remote_tabs.device_id = device_id
648 AND :command_close_tab = remote_tab_commands.command)
649 )";
650 conn.execute(
651 delete_sql,
652 rusqlite::named_params! {
653 ":command_close_tab": CommandKind::CloseTab,
654 },
655 )?;
656
657 info!(
658 "deleted {} pending tab closures because they were not in the new tabs",
659 conn.changes()
660 );
661
662 let sql = format!("
665 DELETE FROM remote_tab_commands
666 WHERE device_id IN (
667 SELECT guid FROM tabs
668 ) AND (SELECT last_modified FROM tabs WHERE guid = device_id) - time_requested >= {REMOTE_COMMAND_TTL_MS}
669 ");
670 tx.execute_cached(&sql, [])?;
671 info!("deleted {} records because they timed out", conn.changes());
672
673 tx.commit()?;
675 conn.execute("DROP TABLE new_remote_tabs", [])?;
676 Ok(())
677 }
678}
679
680#[derive(Debug, Copy, Clone)]
682#[repr(u8)]
683enum CommandKind {
684 CloseTab = 0,
685}
686
687impl AsRef<CommandKind> for RemoteCommand {
688 fn as_ref(&self) -> &CommandKind {
690 match self {
691 RemoteCommand::CloseTab { .. } => &CommandKind::CloseTab,
692 }
693 }
694}
695
696impl FromSql for CommandKind {
697 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
698 Ok(match value.as_i64()? {
699 0 => CommandKind::CloseTab,
700 _ => return Err(FromSqlError::InvalidType),
701 })
702 }
703}
704
705impl ToSql for CommandKind {
706 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
707 Ok(ToSqlOutput::from(*self as u8))
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use std::time::Duration;
714
715 use super::*;
716 use crate::{sync::record::TabsRecordTab, PendingCommand};
717
718 impl RemoteCommand {
719 fn close_tab(url: &str) -> Self {
720 RemoteCommand::CloseTab {
721 url: url.to_string(),
722 }
723 }
724 }
725
726 #[test]
727 fn test_open_if_exists_no_file() {
728 error_support::init_for_tests();
729 let dir = tempfile::tempdir().unwrap();
730 let db_name = dir.path().join("test_open_for_read_no_file.db");
731 let mut storage = TabsStorage::new(db_name.clone());
732 assert!(storage.open_if_exists().unwrap().is_none());
733 storage.open_or_create().unwrap(); let mut storage = TabsStorage::new(db_name);
736 assert!(storage.open_if_exists().unwrap().is_some());
738 }
739
740 #[test]
741 fn test_tabs_meta() {
742 error_support::init_for_tests();
743 let dir = tempfile::tempdir().unwrap();
744 let db_name = dir.path().join("test_tabs_meta.db");
745 let mut db = TabsStorage::new(db_name);
746 let test_key = "TEST KEY A";
747 let test_value = "TEST VALUE A";
748 let test_key2 = "TEST KEY B";
749 let test_value2 = "TEST VALUE B";
750
751 db.put_meta(test_key, &test_value).unwrap();
753 db.put_meta(test_key2, &test_value2).unwrap();
754
755 let retrieved_value: String = db.get_meta(test_key).unwrap().expect("test value");
756 let retrieved_value2: String = db.get_meta(test_key2).unwrap().expect("test value 2");
757
758 assert_eq!(retrieved_value, test_value);
759 assert_eq!(retrieved_value2, test_value2);
760
761 let test_value3 = "TEST VALUE C";
763 db.put_meta(test_key, &test_value3).unwrap();
764
765 let retrieved_value3: String = db.get_meta(test_key).unwrap().expect("test value 3");
766
767 assert_eq!(retrieved_value3, test_value3);
768
769 db.delete_meta(test_key).unwrap();
771 let retrieved_value4: Option<String> = db.get_meta(test_key).unwrap();
772 assert!(retrieved_value4.is_none());
773 }
774
775 struct TabsSQLRecord {
777 guid: String,
778 record: TabsRecord,
779 last_modified: i64,
780 }
781 #[test]
782 fn test_remove_stale_clients() {
783 error_support::init_for_tests();
784 let dir = tempfile::tempdir().unwrap();
785 let db_name = dir.path().join("test_remove_stale_clients.db");
786 let mut storage = TabsStorage::new(db_name);
787 storage.open_or_create().unwrap();
788 assert!(storage.open_if_exists().unwrap().is_some());
789
790 let recent_clients = HashMap::from([
792 (
793 "device-1".to_string(),
794 RemoteClient {
795 fxa_device_id: None,
796 device_name: "my device".to_string(),
797 device_type: sync15::DeviceType::Unknown,
798 },
799 ),
800 (
801 "device-outdated".to_string(),
802 RemoteClient {
803 fxa_device_id: None,
804 device_name: "device with no tabs".to_string(),
805 device_type: DeviceType::Unknown,
806 },
807 ),
808 ]);
809 storage
810 .put_meta(
811 schema::REMOTE_CLIENTS_KEY,
812 &serde_json::to_string(&recent_clients).unwrap(),
813 )
814 .unwrap();
815
816 let records = vec![
817 TabsSQLRecord {
818 guid: "device-1".to_string(),
819 record: TabsRecord {
820 id: "device-1".to_string(),
821 ..Default::default()
822 },
823 last_modified: 1643764207000,
824 },
825 TabsSQLRecord {
826 guid: "device-outdated".to_string(),
827 record: TabsRecord {
828 id: "device-outdated".to_string(),
829 client_name: "Device outdated".to_string(),
830 ..Default::default()
831 },
832 last_modified: 1443764207000, },
834 ];
835 let db = storage.open_if_exists().unwrap().unwrap();
836 for record in records {
837 db.execute(
838 "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
839 rusqlite::named_params! {
840 ":guid": &record.guid,
841 ":record": serde_json::to_string(&record.record).unwrap(),
842 ":last_modified": &record.last_modified,
843 },
844 ).unwrap();
845 }
846 let last_synced = 1643764207000_i64;
848 storage
849 .put_meta(schema::LAST_SYNC_META_KEY, &last_synced)
850 .unwrap();
851 storage.remove_stale_clients().unwrap();
852
853 let remote_tabs = storage.get_remote_tabs().unwrap();
854 assert_eq!(remote_tabs.len(), 1);
856 assert_eq!(remote_tabs[0].client_id, "device-1");
858 }
859
860 fn pending_url_command(device_id: &str, url: &str, ts: Timestamp) -> PendingCommand {
861 PendingCommand {
862 device_id: device_id.to_string(),
863 command: RemoteCommand::CloseTab {
864 url: url.to_string(),
865 },
866 time_requested: ts,
867 time_sent: None,
868 }
869 }
870
871 #[test]
872 fn test_add_pending_dupe_simple() {
873 error_support::init_for_tests();
874 let mut storage = TabsStorage::new_with_mem_path("test_add_pending_dupe_simple");
875 let command = RemoteCommand::close_tab("https://example1.com");
876 assert!(storage
878 .add_remote_tab_command("device-1", &command)
879 .expect("should work"));
880 assert!(!storage
881 .add_remote_tab_command("device-1", &command)
882 .expect("should work"));
883 assert!(storage
884 .remove_remote_tab_command("device-1", &command)
885 .expect("should work"));
886 assert!(storage
887 .add_remote_tab_command("device-1", &command)
888 .expect("should work"));
889 }
890
891 #[test]
892 fn test_add_pending_remote_close() {
893 error_support::init_for_tests();
894 let mut storage = TabsStorage::new_with_mem_path("test_add_pending_remote_close");
895 storage.open_or_create().unwrap();
896 assert!(storage.open_if_exists().unwrap().is_some());
897
898 let now = Timestamp::now();
899 let earliest = now.checked_sub(Duration::from_millis(1)).unwrap();
900 let later = now.checked_add(Duration::from_millis(1)).unwrap();
901 let latest = now.checked_add(Duration::from_millis(2)).unwrap();
902 storage
905 .add_remote_tab_command_at(
906 "device-1",
907 &RemoteCommand::close_tab("https://example1.com"),
908 latest,
909 )
910 .expect("should work");
911 storage
912 .add_remote_tab_command_at(
913 "device-1",
914 &RemoteCommand::close_tab("https://example2.com"),
915 earliest,
916 )
917 .expect("should work");
918 storage
919 .add_remote_tab_command_at(
920 "device-2",
921 &RemoteCommand::close_tab("https://example2.com"),
922 now,
923 )
924 .expect("should work");
925 storage
926 .add_remote_tab_command_at(
927 "device-2",
928 &RemoteCommand::close_tab("https://example3.com"),
929 later,
930 )
931 .expect("should work");
932
933 let got = storage.get_unsent_commands().unwrap();
934
935 assert_eq!(got.len(), 4);
936 assert_eq!(
937 got,
938 vec![
939 pending_url_command("device-1", "https://example2.com", earliest),
940 pending_url_command("device-2", "https://example2.com", now),
941 pending_url_command("device-2", "https://example3.com", later),
942 pending_url_command("device-1", "https://example1.com", latest),
943 ]
944 );
945 }
946
947 #[test]
948 fn test_remote_tabs_filters_pending_closures() {
949 error_support::init_for_tests();
950 let mut storage =
951 TabsStorage::new_with_mem_path("test_remote_tabs_filters_pending_closures");
952 let records = vec![
953 TabsSQLRecord {
954 guid: "device-1".to_string(),
955 record: TabsRecord {
956 id: "device-1".to_string(),
957 client_name: "Device #1".to_string(),
958 tabs: vec![TabsRecordTab {
959 title: "the title".to_string(),
960 url_history: vec!["https://mozilla.org/".to_string()],
961 icon: Some("https://mozilla.org/icon".to_string()),
962 last_used: 1711929600015, ..Default::default()
964 }],
965 ..Default::default()
966 },
967 last_modified: 1711929600015, },
969 TabsSQLRecord {
970 guid: "device-2".to_string(),
971 record: TabsRecord {
972 id: "device-2".to_string(),
973 client_name: "Another device".to_string(),
974 tabs: vec![
975 TabsRecordTab {
976 title: "the title".to_string(),
977 url_history: vec!["https://mozilla.org/".to_string()],
978 icon: Some("https://mozilla.org/icon".to_string()),
979 last_used: 1711929600015, ..Default::default()
981 },
982 TabsRecordTab {
983 title: "the title".to_string(),
984 url_history: vec![
985 "https://example.com/".to_string(),
986 "https://example1.com/".to_string(),
987 ],
988 icon: None,
989 last_used: 1711929600015, ..Default::default()
991 },
992 TabsRecordTab {
993 title: "the title".to_string(),
994 url_history: vec!["https://example1.com/".to_string()],
995 icon: None,
996 last_used: 1711929600015, ..Default::default()
998 },
999 ],
1000 ..Default::default()
1001 },
1002 last_modified: 1711929600015, },
1004 ];
1005
1006 let recent_clients = HashMap::from([
1008 (
1009 "device-1".to_string(),
1010 RemoteClient {
1011 fxa_device_id: None,
1012 device_name: "my device".to_string(),
1013 device_type: sync15::DeviceType::Unknown,
1014 },
1015 ),
1016 (
1017 "device-2".to_string(),
1018 RemoteClient {
1019 fxa_device_id: None,
1020 device_name: "device with no tabs".to_string(),
1021 device_type: DeviceType::Unknown,
1022 },
1023 ),
1024 ]);
1025 storage
1026 .put_meta(
1027 schema::REMOTE_CLIENTS_KEY,
1028 &serde_json::to_string(&recent_clients).unwrap(),
1029 )
1030 .unwrap();
1031
1032 let db = storage.open_if_exists().unwrap().unwrap();
1033 for record in records {
1034 db.execute(
1035 "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
1036 rusqlite::named_params! {
1037 ":guid": &record.guid,
1038 ":record": serde_json::to_string(&record.record).unwrap(),
1039 ":last_modified": &record.last_modified,
1040 },
1041 ).unwrap();
1042 }
1043
1044 storage
1046 .add_remote_tab_command(
1047 "device-1",
1048 &RemoteCommand::close_tab("https://mozilla.org/"),
1049 )
1050 .unwrap();
1051 storage
1052 .add_remote_tab_command(
1053 "device-2",
1054 &RemoteCommand::close_tab("https://example.com/"),
1055 )
1056 .unwrap();
1057 storage
1058 .add_remote_tab_command(
1059 "device-2",
1060 &RemoteCommand::close_tab("https://example1.com/"),
1061 )
1062 .unwrap();
1063
1064 let remote_tabs = storage.get_remote_tabs().unwrap();
1065
1066 assert_eq!(remote_tabs.len(), 2);
1067
1068 assert_eq!(remote_tabs[0].client_id, "device-1");
1070 assert_eq!(remote_tabs[0].remote_tabs.len(), 0);
1071
1072 assert_eq!(remote_tabs[1].client_id, "device-2");
1074 assert_eq!(remote_tabs[1].remote_tabs.len(), 1);
1075 assert_eq!(
1076 remote_tabs[1].remote_tabs[0],
1077 RemoteTab {
1078 title: "the title".to_string(),
1079 url_history: vec!["https://mozilla.org/".to_string()],
1080 icon: Some("https://mozilla.org/icon".to_string()),
1081 last_used: 1711929600015000, ..Default::default()
1083 }
1084 );
1085 }
1086
1087 #[test]
1088 fn test_remove_old_pending_closures_timed_removal() {
1089 error_support::init_for_tests();
1090 let mut storage =
1091 TabsStorage::new_with_mem_path("test_remove_old_pending_closures_timed_removal");
1092
1093 let now = Timestamp::now();
1094 let older = now
1095 .checked_sub(Duration::from_millis(REMOTE_COMMAND_TTL_MS))
1096 .unwrap();
1097
1098 {
1099 let db = storage.open_if_exists().unwrap().unwrap();
1100
1101 db.execute(
1104 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-synced', '', :now);",
1105 rusqlite::named_params! {
1106 ":now" : now,
1107 },
1108 )
1109 .unwrap();
1110
1111 db.execute(
1112 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-not-synced', '', :old);",
1113 rusqlite::named_params! {
1114 ":old" : older,
1115 },
1116 ).unwrap();
1117 }
1118 storage
1121 .add_remote_tab_command_at(
1122 "device-synced",
1123 &RemoteCommand::close_tab("https://example.com"),
1124 older,
1125 )
1126 .unwrap();
1127
1128 storage
1129 .add_remote_tab_command_at(
1130 "device-not-synced",
1131 &RemoteCommand::close_tab("https://example2.com"),
1132 now,
1133 )
1134 .unwrap();
1135
1136 {
1137 let db = storage.open_if_exists().unwrap().unwrap();
1138
1139 let before_count: i64 = db
1141 .conn_ext_query_one("SELECT COUNT(*) FROM remote_tab_commands")
1142 .unwrap();
1143 assert_eq!(before_count, 2);
1144 }
1145 let new_records = vec![(
1147 TabsRecord {
1148 id: "device-not-synced".to_string(),
1149 tabs: vec![TabsRecordTab {
1150 url_history: vec!["https://example2.com".to_string()],
1151 ..Default::default()
1152 }],
1153 ..Default::default()
1154 },
1155 ServerTimestamp::from_millis(now.as_millis_i64()),
1156 )];
1157 storage.remove_old_pending_closures(&new_records).unwrap();
1159
1160 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1161 let after_count: i64 = reopen_db
1162 .conn_ext_query_one("SELECT COUNT(*) FROM remote_tab_commands")
1163 .unwrap();
1164 assert_eq!(after_count, 1);
1165
1166 let remaining_device_id: String = reopen_db
1167 .conn_ext_query_one("SELECT device_id FROM remote_tab_commands")
1168 .unwrap();
1169
1170 assert_eq!(remaining_device_id, "device-not-synced");
1172 }
1173 #[test]
1174 fn test_remove_old_pending_closures_no_tab_removal() {
1175 error_support::init_for_tests();
1176 let mut storage =
1177 TabsStorage::new_with_mem_path("test_remove_old_pending_closures_no_tab_removal");
1178 let db = storage.open_if_exists().unwrap().unwrap();
1179
1180 let now_ms: u64 = Timestamp::now().as_millis();
1181
1182 db.execute(
1184 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-recent', '', :now);",
1185 rusqlite::named_params! {
1186 ":now": now_ms,
1187 },
1188 )
1189 .unwrap();
1190
1191 db.execute(
1193 "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1194 rusqlite::named_params! {
1195 ":command": CommandKind::CloseTab,
1196 ":device_id": "device-recent",
1197 ":url": "https://example.com",
1198 ":time_requested": now_ms,
1199 },
1200 ).unwrap();
1201
1202 db.execute(
1203 "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1204 rusqlite::named_params! {
1205 ":command": CommandKind::CloseTab,
1206 ":device_id": "device-recent",
1207 ":url": "https://old-url.com",
1208 ":time_requested": now_ms,
1209 },
1210 ).unwrap();
1211
1212 let before_count: i64 = db
1214 .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1215 row.get(0)
1216 })
1217 .unwrap();
1218 assert_eq!(before_count, 2);
1219
1220 let new_records = vec![(
1222 TabsRecord {
1223 id: "device-recent".to_string(),
1224 tabs: vec![
1225 TabsRecordTab {
1226 url_history: vec!["https://example99.com".to_string()],
1227 ..Default::default()
1228 },
1229 TabsRecordTab {
1230 url_history: vec!["https://example.com".to_string()],
1231 ..Default::default()
1232 },
1233 ],
1234 ..Default::default()
1235 },
1236 ServerTimestamp::default(),
1237 )];
1238
1239 storage.remove_old_pending_closures(&new_records).unwrap();
1241
1242 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1244 let after_count: i64 = reopen_db
1246 .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1247 row.get(0)
1248 })
1249 .unwrap();
1250 assert_eq!(after_count, 1); let remaining_url: String = reopen_db
1253 .query_row("SELECT url FROM remote_tab_commands", [], |row| row.get(0))
1254 .unwrap();
1255
1256 assert_eq!(remaining_url, "https://example.com"); }
1258
1259 #[test]
1260 fn test_remove_pending_command() {
1261 error_support::init_for_tests();
1262 let mut storage = TabsStorage::new_with_mem_path("test_remove_pending_command");
1263 storage.open_or_create().unwrap();
1264 assert!(storage.open_if_exists().unwrap().is_some());
1265
1266 storage
1267 .add_remote_tab_command(
1268 "device-1",
1269 &RemoteCommand::close_tab("https://example1.com"),
1270 )
1271 .expect("should work");
1272
1273 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1274 assert!(!storage
1275 .remove_remote_tab_command(
1276 "no-devce",
1277 &RemoteCommand::close_tab("https://example1.com"),
1278 )
1279 .unwrap());
1280 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1281
1282 assert!(!storage
1283 .remove_remote_tab_command(
1284 "device-1",
1285 &RemoteCommand::close_tab("https://example9.com"),
1286 )
1287 .unwrap());
1288 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1289
1290 assert!(storage
1291 .remove_remote_tab_command(
1292 "device-1",
1293 &RemoteCommand::close_tab("https://example1.com"),
1294 )
1295 .unwrap());
1296 assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1297 }
1298
1299 #[test]
1300 fn test_sent_command() {
1301 error_support::init_for_tests();
1302 let mut storage = TabsStorage::new_with_mem_path("test_sent_command");
1303 let command = RemoteCommand::close_tab("https://example1.com");
1304 storage
1305 .add_remote_tab_command("device-1", &command)
1306 .expect("should work");
1307
1308 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1309 let pending_command = PendingCommand {
1310 device_id: "device-1".to_string(),
1311 command: command.clone(),
1312 time_requested: Timestamp::now(),
1313 time_sent: None,
1314 };
1315 assert!(storage.set_pending_command_sent(&pending_command).unwrap());
1316 assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1317 assert!(!storage
1319 .add_remote_tab_command("device-1", &command)
1320 .unwrap());
1321 assert!(storage
1323 .remove_remote_tab_command("device-1", &command)
1324 .unwrap());
1325 assert!(storage
1327 .add_remote_tab_command("device-1", &command)
1328 .unwrap());
1329 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1330 }
1331
1332 #[test]
1333 fn test_remove_pending_closures_only_affects_target_device() {
1334 error_support::init_for_tests();
1335 let mut storage =
1336 TabsStorage::new_with_mem_path("test_remove_pending_closures_target_device");
1337 let now = Timestamp::now();
1338
1339 let db = storage.open_if_exists().unwrap().unwrap();
1340
1341 db.execute(
1343 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-1', '', :now);",
1344 rusqlite::named_params! { ":now" : now },
1345 )
1346 .unwrap();
1347
1348 db.execute(
1349 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-2', '', :now);",
1350 rusqlite::named_params! { ":now" : now },
1351 )
1352 .unwrap();
1353
1354 storage
1356 .add_remote_tab_command(
1357 "device-1",
1358 &RemoteCommand::close_tab("https://example1.com"),
1359 )
1360 .unwrap();
1361
1362 storage
1363 .add_remote_tab_command(
1364 "device-1",
1365 &RemoteCommand::close_tab("https://example2.com"),
1366 )
1367 .unwrap();
1368
1369 storage
1370 .add_remote_tab_command(
1371 "device-2",
1372 &RemoteCommand::close_tab("https://example3.com"),
1373 )
1374 .unwrap();
1375
1376 let new_records = vec![(
1378 TabsRecord {
1379 id: "device-1".to_string(),
1380 tabs: vec![TabsRecordTab {
1381 url_history: vec!["https://example1.com".to_string()],
1382 ..Default::default()
1383 }],
1384 ..Default::default()
1385 },
1386 ServerTimestamp::default(),
1387 )];
1388
1389 storage.remove_old_pending_closures(&new_records).unwrap();
1390
1391 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1392 let remaining_commands: Vec<(String, String)> = reopen_db
1393 .prepare("SELECT device_id, url FROM remote_tab_commands")
1394 .unwrap()
1395 .query_map([], |row| {
1396 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1397 })
1398 .unwrap()
1399 .collect::<rusqlite::Result<Vec<_>, _>>()
1400 .unwrap();
1401 assert_eq!(remaining_commands.len(), 2);
1403 assert!(remaining_commands
1404 .contains(&("device-1".to_string(), "https://example1.com".to_string())));
1405 assert!(remaining_commands
1406 .contains(&("device-2".to_string(), "https://example3.com".to_string())));
1407 }
1408
1409 #[test]
1410 fn test_close_connection() {
1411 let dir = tempfile::tempdir().unwrap();
1412 let db_path = dir.path().join("test_close_connection.db");
1413 let mut storage = TabsStorage::new(db_path);
1414
1415 storage.open_or_create().unwrap();
1417
1418 assert!(matches!(storage.db_connection, DbConnection::Open(_)));
1420
1421 storage.close();
1423
1424 assert!(matches!(storage.db_connection, DbConnection::Closed));
1426
1427 let result = storage.open_or_create();
1429 assert!(result.is_err());
1430 assert!(matches!(
1431 result.unwrap_err(),
1432 Error::UnexpectedConnectionState
1433 ));
1434 }
1435}