places/
ffi.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// This module implement the traits that make the FFI code easier to manage.
6
7use crate::api::matcher::{self, search_frecent, SearchParams};
8pub use crate::api::places_api::places_api_new;
9pub use crate::error::{warn, Result};
10pub use crate::error::{ApiResult, PlacesApiError};
11#[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
12use crate::glean_metrics::places_manager;
13pub use crate::import::common::HistoryMigrationResult;
14use crate::import::import_ios_history;
15use crate::storage;
16use crate::storage::bookmarks;
17pub use crate::storage::bookmarks::BookmarkPosition;
18pub use crate::storage::history_metadata::{
19    DocumentType, HistoryHighlight, HistoryHighlightWeights, HistoryMetadata,
20    HistoryMetadataObservation, HistoryMetadataPageMissingBehavior,
21    NoteHistoryMetadataObservationOptions,
22};
23pub use crate::storage::RunMaintenanceMetrics;
24use crate::storage::{history, history_metadata};
25use crate::types::VisitTransitionSet;
26use crate::ConnectionType;
27use crate::VisitObservation;
28use crate::VisitType;
29use crate::{PlacesApi, PlacesDb};
30use error_support::handle_error;
31use interrupt_support::register_interrupt;
32pub use interrupt_support::SqlInterruptHandle;
33use parking_lot::Mutex;
34use std::sync::{Arc, Weak};
35pub use sync_guid::Guid;
36pub use types::Timestamp as PlacesTimestamp;
37pub use url::Url;
38
39// From https://searchfox.org/mozilla-central/rev/1674b86019a96f076e0f98f1d0f5f3ab9d4e9020/browser/components/newtab/lib/TopSitesFeed.jsm#87
40const SKIP_ONE_PAGE_FRECENCY_THRESHOLD: i64 = 101 + 1;
41
42// `bookmarks::InsertableItem` is clear for Rust code, but just `InsertableItem` is less
43// clear in the UDL - so change some of the type names.
44pub type InsertableBookmarkItem = crate::storage::bookmarks::InsertableItem;
45pub type InsertableBookmarkFolder = crate::storage::bookmarks::InsertableFolder;
46pub type InsertableBookmarkSeparator = crate::storage::bookmarks::InsertableSeparator;
47pub use crate::storage::bookmarks::InsertableBookmark;
48
49pub use crate::storage::bookmarks::BookmarkUpdateInfo;
50
51// And types used when fetching items.
52pub type BookmarkItem = crate::storage::bookmarks::fetch::Item;
53pub type BookmarkFolder = crate::storage::bookmarks::fetch::Folder;
54pub type BookmarkSeparator = crate::storage::bookmarks::fetch::Separator;
55pub use crate::storage::bookmarks::fetch::BookmarkData;
56
57// Check for multiple write connections open at the same time
58//
59// One potential cause of #5040 is that Fenix is somehow opening multiiple write connections to
60// the places DB.  This code tests if that's happening and reports an error if so.
61lazy_static::lazy_static! {
62    static ref READ_WRITE_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
63    static ref SYNC_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
64}
65
66impl PlacesApi {
67    #[handle_error(crate::Error)]
68    pub fn new_connection(&self, conn_type: ConnectionType) -> ApiResult<Arc<PlacesConnection>> {
69        let db = self.open_connection(conn_type)?;
70        let connection = Arc::new(PlacesConnection::new(db));
71        register_interrupt(Arc::<PlacesConnection>::downgrade(&connection));
72        Ok(connection)
73    }
74}
75
76pub struct PlacesConnection {
77    db: Mutex<PlacesDb>,
78    interrupt_handle: Arc<SqlInterruptHandle>,
79}
80
81impl PlacesConnection {
82    pub fn new(db: PlacesDb) -> Self {
83        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
84        places_manager::connection_initialized.add(1);
85
86        Self {
87            interrupt_handle: db.new_interrupt_handle(),
88            db: Mutex::new(db),
89        }
90    }
91
92    // A helper that gets the connection from the mutex and converts errors.
93    fn with_conn<F, T>(&self, f: F) -> Result<T>
94    where
95        F: FnOnce(&PlacesDb) -> crate::error::Result<T>,
96    {
97        let conn = self.db.lock();
98        f(&conn)
99    }
100
101    // pass the SqlInterruptHandle as an object through Uniffi
102    pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
103        Arc::clone(&self.interrupt_handle)
104    }
105
106    #[handle_error(crate::Error)]
107    pub fn get_latest_history_metadata_for_url(
108        &self,
109        url: Url,
110    ) -> ApiResult<Option<HistoryMetadata>> {
111        self.with_conn(|conn| history_metadata::get_latest_for_url(conn, &url))
112    }
113
114    #[handle_error(crate::Error)]
115    pub fn get_history_metadata_between(
116        &self,
117        start: PlacesTimestamp,
118        end: PlacesTimestamp,
119    ) -> ApiResult<Vec<HistoryMetadata>> {
120        self.with_conn(|conn| {
121            history_metadata::get_between(conn, start.as_millis_i64(), end.as_millis_i64())
122        })
123    }
124
125    #[handle_error(crate::Error)]
126    pub fn get_history_metadata_since(
127        &self,
128        start: PlacesTimestamp,
129    ) -> ApiResult<Vec<HistoryMetadata>> {
130        self.with_conn(|conn| history_metadata::get_since(conn, start.as_millis_i64()))
131    }
132
133    #[handle_error(crate::Error)]
134    pub fn get_most_recent_history_metadata(&self, limit: i32) -> ApiResult<Vec<HistoryMetadata>> {
135        self.with_conn(|conn| history_metadata::get_most_recent(conn, limit))
136    }
137
138    #[handle_error(crate::Error)]
139    pub fn get_most_recent_search_entries_in_history_metadata(
140        &self,
141        limit: i32,
142    ) -> ApiResult<Vec<HistoryMetadata>> {
143        self.with_conn(|conn| history_metadata::get_most_recent_search_entries(conn, limit))
144    }
145
146    #[handle_error(crate::Error)]
147    pub fn query_history_metadata(
148        &self,
149        query: String,
150        limit: i32,
151    ) -> ApiResult<Vec<HistoryMetadata>> {
152        self.with_conn(|conn| history_metadata::query(conn, query.as_str(), limit))
153    }
154
155    #[handle_error(crate::Error)]
156    pub fn get_history_highlights(
157        &self,
158        weights: HistoryHighlightWeights,
159        limit: i32,
160    ) -> ApiResult<Vec<HistoryHighlight>> {
161        self.with_conn(|conn| history_metadata::get_highlights(conn, weights, limit))
162    }
163
164    #[handle_error(crate::Error)]
165    pub fn note_history_metadata_observation(
166        &self,
167        data: HistoryMetadataObservation,
168        options: NoteHistoryMetadataObservationOptions,
169    ) -> ApiResult<()> {
170        // odd historical naming discrepancy - public function is "note_*", impl is "apply_*"
171        self.with_conn(|conn| history_metadata::apply_metadata_observation(conn, data, options))
172    }
173
174    #[handle_error(crate::Error)]
175    pub fn metadata_delete_older_than(&self, older_than: PlacesTimestamp) -> ApiResult<()> {
176        self.with_conn(|conn| history_metadata::delete_older_than(conn, older_than.as_millis_i64()))
177    }
178
179    #[handle_error(crate::Error)]
180    pub fn metadata_delete(
181        &self,
182        url: Url,
183        referrer_url: Option<Url>,
184        search_term: Option<String>,
185    ) -> ApiResult<()> {
186        self.with_conn(|conn| {
187            history_metadata::delete_metadata(
188                conn,
189                &url,
190                referrer_url.as_ref(),
191                search_term.as_deref(),
192            )
193        })
194    }
195
196    #[handle_error(crate::Error)]
197    pub fn metadata_delete_search_terms(&self) -> ApiResult<()> {
198        self.with_conn(history_metadata::delete_all_metadata_for_search)
199    }
200
201    /// Add an observation to the database.
202    #[handle_error(crate::Error)]
203    pub fn apply_observation(&self, visit: VisitObservation) -> ApiResult<()> {
204        self.with_conn(|conn| history::apply_observation(conn, visit))?;
205        Ok(())
206    }
207
208    #[handle_error(crate::Error)]
209    pub fn get_visited_urls_in_range(
210        &self,
211        start: PlacesTimestamp,
212        end: PlacesTimestamp,
213        include_remote: bool,
214    ) -> ApiResult<Vec<Url>> {
215        self.with_conn(|conn| {
216            let urls = history::get_visited_urls(conn, start, end, include_remote)?
217                .iter()
218                // Turn the list of strings into valid Urls
219                .filter_map(|s| Url::parse(s).ok())
220                .collect::<Vec<_>>();
221            Ok(urls)
222        })
223    }
224
225    #[handle_error(crate::Error)]
226    pub fn get_visit_infos(
227        &self,
228        start_date: PlacesTimestamp,
229        end_date: PlacesTimestamp,
230        exclude_types: VisitTransitionSet,
231    ) -> ApiResult<Vec<HistoryVisitInfo>> {
232        self.with_conn(|conn| history::get_visit_infos(conn, start_date, end_date, exclude_types))
233    }
234
235    #[handle_error(crate::Error)]
236    pub fn get_visit_count(&self, exclude_types: VisitTransitionSet) -> ApiResult<i64> {
237        self.with_conn(|conn| history::get_visit_count(conn, exclude_types))
238    }
239
240    #[handle_error(crate::Error)]
241    pub fn get_visit_count_for_host(
242        &self,
243        host: String,
244        before: PlacesTimestamp,
245        exclude_types: VisitTransitionSet,
246    ) -> ApiResult<i64> {
247        self.with_conn(|conn| {
248            history::get_visit_count_for_host(conn, host.as_str(), before, exclude_types)
249        })
250    }
251
252    #[handle_error(crate::Error)]
253    pub fn get_visit_page(
254        &self,
255        offset: i64,
256        count: i64,
257        exclude_types: VisitTransitionSet,
258    ) -> ApiResult<Vec<HistoryVisitInfo>> {
259        self.with_conn(|conn| history::get_visit_page(conn, offset, count, exclude_types))
260    }
261
262    #[handle_error(crate::Error)]
263    pub fn get_visit_page_with_bound(
264        &self,
265        bound: i64,
266        offset: i64,
267        count: i64,
268        exclude_types: VisitTransitionSet,
269    ) -> ApiResult<HistoryVisitInfosWithBound> {
270        self.with_conn(|conn| {
271            history::get_visit_page_with_bound(conn, bound, offset, count, exclude_types)
272        })
273    }
274
275    // This is identical to get_visited in history.rs but takes a list of strings instead of urls
276    // This is necessary b/c we still need to return 'false' for bad URLs which prevents us from
277    // parsing/filtering them before reaching the history layer
278    #[handle_error(crate::Error)]
279    pub fn get_visited(&self, urls: Vec<String>) -> ApiResult<Vec<bool>> {
280        let iter = urls.into_iter();
281        let mut result = vec![false; iter.len()];
282        let url_idxs = iter
283            .enumerate()
284            .filter_map(|(idx, s)| Url::parse(&s).ok().map(|url| (idx, url)))
285            .collect::<Vec<_>>();
286        self.with_conn(|conn| history::get_visited_into(conn, &url_idxs, &mut result))?;
287        Ok(result)
288    }
289
290    #[handle_error(crate::Error)]
291    pub fn delete_visits_for(&self, url: String) -> ApiResult<()> {
292        self.with_conn(|conn| {
293            let guid = match Url::parse(&url) {
294                Ok(url) => history::url_to_guid(conn, &url)?,
295                Err(e) => {
296                    warn!("Invalid URL passed to places_delete_visits_for, {}", e);
297                    history::href_to_guid(conn, url.clone().as_str())?
298                }
299            };
300            if let Some(guid) = guid {
301                history::delete_visits_for(conn, &guid)?;
302            }
303            Ok(())
304        })
305    }
306
307    #[handle_error(crate::Error)]
308    pub fn delete_visits_between(
309        &self,
310        start: PlacesTimestamp,
311        end: PlacesTimestamp,
312    ) -> ApiResult<()> {
313        self.with_conn(|conn| history::delete_visits_between(conn, start, end))
314    }
315
316    #[handle_error(crate::Error)]
317    pub fn delete_visit(&self, url: String, timestamp: PlacesTimestamp) -> ApiResult<()> {
318        self.with_conn(|conn| {
319            match Url::parse(&url) {
320                Ok(url) => {
321                    history::delete_place_visit_at_time(conn, &url, timestamp)?;
322                }
323                Err(e) => {
324                    warn!("Invalid URL passed to places_delete_visit, {}", e);
325                    history::delete_place_visit_at_time_by_href(conn, url.as_str(), timestamp)?;
326                }
327            };
328            Ok(())
329        })
330    }
331
332    #[handle_error(crate::Error)]
333    pub fn get_top_frecent_site_infos(
334        &self,
335        num_items: i32,
336        threshold_option: FrecencyThresholdOption,
337    ) -> ApiResult<Vec<TopFrecentSiteInfo>> {
338        self.with_conn(|conn| {
339            crate::storage::history::get_top_frecent_site_infos(
340                conn,
341                num_items,
342                threshold_option.value(),
343            )
344        })
345    }
346    // deletes all history and updates the sync metadata to only sync after
347    // most recent visit to prevent further syncing of older data
348    #[handle_error(crate::Error)]
349    pub fn delete_everything_history(&self) -> ApiResult<()> {
350        history::delete_everything(&self.db.lock())
351    }
352
353    #[handle_error(crate::Error)]
354    pub fn run_maintenance_prune(
355        &self,
356        db_size_limit: u32,
357        prune_limit: u32,
358    ) -> ApiResult<RunMaintenanceMetrics> {
359        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
360        let timer_id = places_manager::run_maintenance_prune_time_temp.start();
361        let res =
362            self.with_conn(|conn| storage::run_maintenance_prune(conn, db_size_limit, prune_limit));
363
364        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
365        places_manager::run_maintenance_prune_time_temp.stop_and_accumulate(timer_id);
366
367        res
368    }
369
370    #[handle_error(crate::Error)]
371    pub fn run_maintenance_vacuum(&self) -> ApiResult<()> {
372        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
373        let timer_id = places_manager::run_maintenance_vacuum_time_temp.start();
374        let res = self.with_conn(storage::run_maintenance_vacuum);
375
376        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
377        places_manager::run_maintenance_vacuum_time_temp.stop_and_accumulate(timer_id);
378
379        res
380    }
381
382    #[handle_error(crate::Error)]
383    pub fn run_maintenance_optimize(&self) -> ApiResult<()> {
384        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
385        let timer_id = places_manager::run_maintenance_optimize_time_temp.start();
386        let res = self.with_conn(storage::run_maintenance_optimize);
387
388        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
389        places_manager::run_maintenance_optimize_time_temp.stop_and_accumulate(timer_id);
390
391        res
392    }
393
394    #[handle_error(crate::Error)]
395    pub fn run_maintenance_checkpoint(&self) -> ApiResult<()> {
396        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
397        let timer_id = places_manager::run_maintenance_chk_pnt_time_temp.start();
398        let res = self.with_conn(storage::run_maintenance_checkpoint);
399
400        #[cfg(all(feature = "glean-sym", any(target_os = "android", target_os = "ios")))]
401        places_manager::run_maintenance_chk_pnt_time_temp.stop_and_accumulate(timer_id);
402
403        res
404    }
405
406    #[handle_error(crate::Error)]
407    pub fn query_autocomplete(&self, search: String, limit: i32) -> ApiResult<Vec<SearchResult>> {
408        self.with_conn(|conn| {
409            search_frecent(
410                conn,
411                SearchParams {
412                    search_string: search,
413                    limit: limit as u32,
414                },
415            )
416            .map(|search_results| search_results.into_iter().map(Into::into).collect())
417        })
418    }
419
420    #[handle_error(crate::Error)]
421    pub fn accept_result(&self, search_string: String, url: String) -> ApiResult<()> {
422        self.with_conn(|conn| {
423            match Url::parse(&url) {
424                Ok(url) => {
425                    matcher::accept_result(conn, &search_string, &url)?;
426                }
427                Err(_) => {
428                    warn!("Ignoring invalid URL in places_accept_result");
429                    return Ok(());
430                }
431            };
432            Ok(())
433        })
434    }
435
436    #[handle_error(crate::Error)]
437    pub fn match_url(&self, query: String) -> ApiResult<Option<Url>> {
438        self.with_conn(|conn| matcher::match_url(conn, query))
439    }
440
441    #[handle_error(crate::Error)]
442    pub fn bookmarks_get_tree(&self, item_guid: &Guid) -> ApiResult<Option<BookmarkItem>> {
443        self.with_conn(|conn| bookmarks::fetch::fetch_tree(conn, item_guid))
444    }
445
446    #[handle_error(crate::Error)]
447    pub fn bookmarks_get_by_guid(
448        &self,
449        guid: &Guid,
450        get_direct_children: bool,
451    ) -> ApiResult<Option<BookmarkItem>> {
452        self.with_conn(|conn| {
453            let bookmark = bookmarks::fetch::fetch_bookmark(conn, guid, get_direct_children)?;
454            Ok(bookmark)
455        })
456    }
457
458    #[handle_error(crate::Error)]
459    pub fn bookmarks_get_all_with_url(&self, url: String) -> ApiResult<Vec<BookmarkItem>> {
460        self.with_conn(|conn| {
461            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
462            match Url::parse(&url) {
463                Ok(url) => Ok(bookmarks::fetch::fetch_bookmarks_by_url(conn, &url)?
464                    .into_iter()
465                    .map(|b| BookmarkItem::Bookmark { b })
466                    .collect::<Vec<BookmarkItem>>()),
467                Err(e) => {
468                    // There are no bookmarks with the URL if it's invalid.
469                    warn!("Invalid URL passed to bookmarks_get_all_with_url, {}", e);
470                    Ok(Vec::<BookmarkItem>::new())
471                }
472            }
473        })
474    }
475
476    #[handle_error(crate::Error)]
477    pub fn bookmarks_search(&self, query: String, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
478        self.with_conn(|conn| {
479            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
480            Ok(
481                bookmarks::fetch::search_bookmarks(conn, query.as_str(), limit as u32)?
482                    .into_iter()
483                    .map(|b| BookmarkItem::Bookmark { b })
484                    .collect(),
485            )
486        })
487    }
488
489    #[handle_error(crate::Error)]
490    pub fn bookmarks_get_recent(&self, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
491        self.with_conn(|conn| {
492            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
493            Ok(bookmarks::fetch::recent_bookmarks(conn, limit as u32)?
494                .into_iter()
495                .map(|b| BookmarkItem::Bookmark { b })
496                .collect())
497        })
498    }
499
500    #[handle_error(crate::Error)]
501    pub fn bookmarks_delete(&self, id: Guid) -> ApiResult<bool> {
502        self.with_conn(|conn| bookmarks::delete_bookmark(conn, &id))
503    }
504
505    #[handle_error(crate::Error)]
506    pub fn bookmarks_delete_everything(&self) -> ApiResult<()> {
507        self.with_conn(bookmarks::delete_everything)
508    }
509
510    #[handle_error(crate::Error)]
511    pub fn bookmarks_get_url_for_keyword(&self, keyword: String) -> ApiResult<Option<Url>> {
512        self.with_conn(|conn| bookmarks::bookmarks_get_url_for_keyword(conn, keyword.as_str()))
513    }
514
515    #[handle_error(crate::Error)]
516    pub fn bookmarks_insert(&self, data: InsertableBookmarkItem) -> ApiResult<Guid> {
517        self.with_conn(|conn| bookmarks::insert_bookmark(conn, data))
518    }
519
520    #[handle_error(crate::Error)]
521    pub fn bookmarks_update(&self, item: BookmarkUpdateInfo) -> ApiResult<()> {
522        self.with_conn(|conn| bookmarks::update_bookmark_from_info(conn, item))
523    }
524
525    #[handle_error(crate::Error)]
526    pub fn bookmarks_count_bookmarks_in_trees(&self, guids: &[Guid]) -> ApiResult<u32> {
527        self.with_conn(|conn| bookmarks::count_bookmarks_in_trees(conn, guids))
528    }
529
530    #[handle_error(crate::Error)]
531    pub fn places_history_import_from_ios(
532        &self,
533        db_path: String,
534        last_sync_timestamp: i64,
535    ) -> ApiResult<HistoryMigrationResult> {
536        self.with_conn(|conn| import_ios_history(conn, &db_path, last_sync_timestamp))
537    }
538}
539
540impl AsRef<SqlInterruptHandle> for PlacesConnection {
541    fn as_ref(&self) -> &SqlInterruptHandle {
542        &self.interrupt_handle
543    }
544}
545
546#[derive(Clone, PartialEq, Eq)]
547pub struct HistoryVisitInfo {
548    pub url: Url,
549    pub title: Option<String>,
550    pub timestamp: PlacesTimestamp,
551    pub visit_type: VisitType,
552    pub is_hidden: bool,
553    pub preview_image_url: Option<Url>,
554    pub is_remote: bool,
555}
556#[derive(Clone, PartialEq, Eq)]
557pub struct HistoryVisitInfosWithBound {
558    pub infos: Vec<HistoryVisitInfo>,
559    pub bound: i64,
560    pub offset: i64,
561}
562
563pub struct TopFrecentSiteInfo {
564    pub url: Url,
565    pub title: Option<String>,
566}
567
568pub enum FrecencyThresholdOption {
569    None,
570    SkipOneTimePages,
571}
572
573impl FrecencyThresholdOption {
574    fn value(&self) -> i64 {
575        match self {
576            FrecencyThresholdOption::None => 0,
577            FrecencyThresholdOption::SkipOneTimePages => SKIP_ONE_PAGE_FRECENCY_THRESHOLD,
578        }
579    }
580}
581
582pub struct SearchResult {
583    pub url: Url,
584    pub title: String,
585    pub frecency: i64,
586}
587
588// Exists just to convince uniffi to generate `liftSequence*` helpers!
589pub struct Dummy {
590    pub md: Option<Vec<HistoryMetadata>>,
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use crate::test::new_mem_connection;
597
598    #[test]
599    fn test_accept_result_with_invalid_url() {
600        let conn = PlacesConnection::new(new_mem_connection());
601        let invalid_url = "http://1234.56.78.90".to_string();
602        assert!(PlacesConnection::accept_result(&conn, "ample".to_string(), invalid_url).is_ok());
603    }
604
605    #[test]
606    fn test_bookmarks_get_all_with_url_with_invalid_url() {
607        let conn = PlacesConnection::new(new_mem_connection());
608        let invalid_url = "http://1234.56.78.90".to_string();
609        assert!(PlacesConnection::bookmarks_get_all_with_url(&conn, invalid_url).is_ok());
610    }
611}