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