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