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 query_history_metadata(
213        &self,
214        query: String,
215        limit: i32,
216    ) -> ApiResult<Vec<HistoryMetadata>> {
217        self.with_conn(|conn| history_metadata::query(conn, query.as_str(), limit))
218    }
219
220    #[handle_error(crate::Error)]
221    pub fn get_history_highlights(
222        &self,
223        weights: HistoryHighlightWeights,
224        limit: i32,
225    ) -> ApiResult<Vec<HistoryHighlight>> {
226        self.with_conn(|conn| history_metadata::get_highlights(conn, weights, limit))
227    }
228
229    #[handle_error(crate::Error)]
230    pub fn note_history_metadata_observation(
231        &self,
232        data: HistoryMetadataObservation,
233        options: NoteHistoryMetadataObservationOptions,
234    ) -> ApiResult<()> {
235        // odd historical naming discrepancy - public function is "note_*", impl is "apply_*"
236        self.with_conn(|conn| history_metadata::apply_metadata_observation(conn, data, options))
237    }
238
239    #[handle_error(crate::Error)]
240    pub fn metadata_delete_older_than(&self, older_than: PlacesTimestamp) -> ApiResult<()> {
241        self.with_conn(|conn| history_metadata::delete_older_than(conn, older_than.as_millis_i64()))
242    }
243
244    #[handle_error(crate::Error)]
245    pub fn metadata_delete(
246        &self,
247        url: Url,
248        referrer_url: Option<Url>,
249        search_term: Option<String>,
250    ) -> ApiResult<()> {
251        self.with_conn(|conn| {
252            history_metadata::delete_metadata(
253                conn,
254                &url,
255                referrer_url.as_ref(),
256                search_term.as_deref(),
257            )
258        })
259    }
260
261    /// Add an observation to the database.
262    #[handle_error(crate::Error)]
263    pub fn apply_observation(&self, visit: VisitObservation) -> ApiResult<()> {
264        self.with_conn(|conn| history::apply_observation(conn, visit))?;
265        Ok(())
266    }
267
268    #[handle_error(crate::Error)]
269    pub fn get_visited_urls_in_range(
270        &self,
271        start: PlacesTimestamp,
272        end: PlacesTimestamp,
273        include_remote: bool,
274    ) -> ApiResult<Vec<Url>> {
275        self.with_conn(|conn| {
276            let urls = history::get_visited_urls(conn, start, end, include_remote)?
277                .iter()
278                // Turn the list of strings into valid Urls
279                .filter_map(|s| Url::parse(s).ok())
280                .collect::<Vec<_>>();
281            Ok(urls)
282        })
283    }
284
285    #[handle_error(crate::Error)]
286    pub fn get_visit_infos(
287        &self,
288        start_date: PlacesTimestamp,
289        end_date: PlacesTimestamp,
290        exclude_types: VisitTransitionSet,
291    ) -> ApiResult<Vec<HistoryVisitInfo>> {
292        self.with_conn(|conn| history::get_visit_infos(conn, start_date, end_date, exclude_types))
293    }
294
295    #[handle_error(crate::Error)]
296    pub fn get_visit_count(&self, exclude_types: VisitTransitionSet) -> ApiResult<i64> {
297        self.with_conn(|conn| history::get_visit_count(conn, exclude_types))
298    }
299
300    #[handle_error(crate::Error)]
301    pub fn get_visit_count_for_host(
302        &self,
303        host: String,
304        before: PlacesTimestamp,
305        exclude_types: VisitTransitionSet,
306    ) -> ApiResult<i64> {
307        self.with_conn(|conn| {
308            history::get_visit_count_for_host(conn, host.as_str(), before, exclude_types)
309        })
310    }
311
312    #[handle_error(crate::Error)]
313    pub fn get_visit_page(
314        &self,
315        offset: i64,
316        count: i64,
317        exclude_types: VisitTransitionSet,
318    ) -> ApiResult<Vec<HistoryVisitInfo>> {
319        self.with_conn(|conn| history::get_visit_page(conn, offset, count, exclude_types))
320    }
321
322    #[handle_error(crate::Error)]
323    pub fn get_visit_page_with_bound(
324        &self,
325        bound: i64,
326        offset: i64,
327        count: i64,
328        exclude_types: VisitTransitionSet,
329    ) -> ApiResult<HistoryVisitInfosWithBound> {
330        self.with_conn(|conn| {
331            history::get_visit_page_with_bound(conn, bound, offset, count, exclude_types)
332        })
333    }
334
335    // This is identical to get_visited in history.rs but takes a list of strings instead of urls
336    // This is necessary b/c we still need to return 'false' for bad URLs which prevents us from
337    // parsing/filtering them before reaching the history layer
338    #[handle_error(crate::Error)]
339    pub fn get_visited(&self, urls: Vec<String>) -> ApiResult<Vec<bool>> {
340        let iter = urls.into_iter();
341        let mut result = vec![false; iter.len()];
342        let url_idxs = iter
343            .enumerate()
344            .filter_map(|(idx, s)| Url::parse(&s).ok().map(|url| (idx, url)))
345            .collect::<Vec<_>>();
346        self.with_conn(|conn| history::get_visited_into(conn, &url_idxs, &mut result))?;
347        Ok(result)
348    }
349
350    #[handle_error(crate::Error)]
351    pub fn delete_visits_for(&self, url: String) -> ApiResult<()> {
352        self.with_conn(|conn| {
353            let guid = match Url::parse(&url) {
354                Ok(url) => history::url_to_guid(conn, &url)?,
355                Err(e) => {
356                    warn!("Invalid URL passed to places_delete_visits_for, {}", e);
357                    history::href_to_guid(conn, url.clone().as_str())?
358                }
359            };
360            if let Some(guid) = guid {
361                history::delete_visits_for(conn, &guid)?;
362            }
363            Ok(())
364        })
365    }
366
367    #[handle_error(crate::Error)]
368    pub fn delete_visits_between(
369        &self,
370        start: PlacesTimestamp,
371        end: PlacesTimestamp,
372    ) -> ApiResult<()> {
373        self.with_conn(|conn| history::delete_visits_between(conn, start, end))
374    }
375
376    #[handle_error(crate::Error)]
377    pub fn delete_visit(&self, url: String, timestamp: PlacesTimestamp) -> ApiResult<()> {
378        self.with_conn(|conn| {
379            match Url::parse(&url) {
380                Ok(url) => {
381                    history::delete_place_visit_at_time(conn, &url, timestamp)?;
382                }
383                Err(e) => {
384                    warn!("Invalid URL passed to places_delete_visit, {}", e);
385                    history::delete_place_visit_at_time_by_href(conn, url.as_str(), timestamp)?;
386                }
387            };
388            Ok(())
389        })
390    }
391
392    #[handle_error(crate::Error)]
393    pub fn get_top_frecent_site_infos(
394        &self,
395        num_items: i32,
396        threshold_option: FrecencyThresholdOption,
397    ) -> ApiResult<Vec<TopFrecentSiteInfo>> {
398        self.with_conn(|conn| {
399            crate::storage::history::get_top_frecent_site_infos(
400                conn,
401                num_items,
402                threshold_option.value(),
403            )
404        })
405    }
406    // deletes all history and updates the sync metadata to only sync after
407    // most recent visit to prevent further syncing of older data
408    #[handle_error(crate::Error)]
409    pub fn delete_everything_history(&self) -> ApiResult<()> {
410        history::delete_everything(&self.db.lock())
411    }
412
413    #[handle_error(crate::Error)]
414    pub fn run_maintenance_prune(
415        &self,
416        db_size_limit: u32,
417        prune_limit: u32,
418    ) -> ApiResult<RunMaintenanceMetrics> {
419        self.with_conn(|conn| storage::run_maintenance_prune(conn, db_size_limit, prune_limit))
420    }
421
422    #[handle_error(crate::Error)]
423    pub fn run_maintenance_vacuum(&self) -> ApiResult<()> {
424        self.with_conn(storage::run_maintenance_vacuum)
425    }
426
427    #[handle_error(crate::Error)]
428    pub fn run_maintenance_optimize(&self) -> ApiResult<()> {
429        self.with_conn(storage::run_maintenance_optimize)
430    }
431
432    #[handle_error(crate::Error)]
433    pub fn run_maintenance_checkpoint(&self) -> ApiResult<()> {
434        self.with_conn(storage::run_maintenance_checkpoint)
435    }
436
437    #[handle_error(crate::Error)]
438    pub fn query_autocomplete(&self, search: String, limit: i32) -> ApiResult<Vec<SearchResult>> {
439        self.with_conn(|conn| {
440            search_frecent(
441                conn,
442                SearchParams {
443                    search_string: search,
444                    limit: limit as u32,
445                },
446            )
447            .map(|search_results| search_results.into_iter().map(Into::into).collect())
448        })
449    }
450
451    #[handle_error(crate::Error)]
452    pub fn accept_result(&self, search_string: String, url: String) -> ApiResult<()> {
453        self.with_conn(|conn| {
454            match Url::parse(&url) {
455                Ok(url) => {
456                    matcher::accept_result(conn, &search_string, &url)?;
457                }
458                Err(_) => {
459                    warn!("Ignoring invalid URL in places_accept_result");
460                    return Ok(());
461                }
462            };
463            Ok(())
464        })
465    }
466
467    #[handle_error(crate::Error)]
468    pub fn match_url(&self, query: String) -> ApiResult<Option<Url>> {
469        self.with_conn(|conn| matcher::match_url(conn, query))
470    }
471
472    #[handle_error(crate::Error)]
473    pub fn bookmarks_get_tree(&self, item_guid: &Guid) -> ApiResult<Option<BookmarkItem>> {
474        self.with_conn(|conn| bookmarks::fetch::fetch_tree(conn, item_guid))
475    }
476
477    #[handle_error(crate::Error)]
478    pub fn bookmarks_get_by_guid(
479        &self,
480        guid: &Guid,
481        get_direct_children: bool,
482    ) -> ApiResult<Option<BookmarkItem>> {
483        self.with_conn(|conn| {
484            let bookmark = bookmarks::fetch::fetch_bookmark(conn, guid, get_direct_children)?;
485            Ok(bookmark)
486        })
487    }
488
489    #[handle_error(crate::Error)]
490    pub fn bookmarks_get_all_with_url(&self, url: String) -> ApiResult<Vec<BookmarkItem>> {
491        self.with_conn(|conn| {
492            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
493            match Url::parse(&url) {
494                Ok(url) => Ok(bookmarks::fetch::fetch_bookmarks_by_url(conn, &url)?
495                    .into_iter()
496                    .map(|b| BookmarkItem::Bookmark { b })
497                    .collect::<Vec<BookmarkItem>>()),
498                Err(e) => {
499                    // There are no bookmarks with the URL if it's invalid.
500                    warn!("Invalid URL passed to bookmarks_get_all_with_url, {}", e);
501                    Ok(Vec::<BookmarkItem>::new())
502                }
503            }
504        })
505    }
506
507    #[handle_error(crate::Error)]
508    pub fn bookmarks_search(&self, query: String, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
509        self.with_conn(|conn| {
510            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
511            Ok(
512                bookmarks::fetch::search_bookmarks(conn, query.as_str(), limit as u32)?
513                    .into_iter()
514                    .map(|b| BookmarkItem::Bookmark { b })
515                    .collect(),
516            )
517        })
518    }
519
520    #[handle_error(crate::Error)]
521    pub fn bookmarks_get_recent(&self, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
522        self.with_conn(|conn| {
523            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
524            Ok(bookmarks::fetch::recent_bookmarks(conn, limit as u32)?
525                .into_iter()
526                .map(|b| BookmarkItem::Bookmark { b })
527                .collect())
528        })
529    }
530
531    #[handle_error(crate::Error)]
532    pub fn bookmarks_delete(&self, id: Guid) -> ApiResult<bool> {
533        self.with_conn(|conn| bookmarks::delete_bookmark(conn, &id))
534    }
535
536    #[handle_error(crate::Error)]
537    pub fn bookmarks_delete_everything(&self) -> ApiResult<()> {
538        self.with_conn(bookmarks::delete_everything)
539    }
540
541    #[handle_error(crate::Error)]
542    pub fn bookmarks_get_url_for_keyword(&self, keyword: String) -> ApiResult<Option<Url>> {
543        self.with_conn(|conn| bookmarks::bookmarks_get_url_for_keyword(conn, keyword.as_str()))
544    }
545
546    #[handle_error(crate::Error)]
547    pub fn bookmarks_insert(&self, data: InsertableBookmarkItem) -> ApiResult<Guid> {
548        self.with_conn(|conn| bookmarks::insert_bookmark(conn, data))
549    }
550
551    #[handle_error(crate::Error)]
552    pub fn bookmarks_update(&self, item: BookmarkUpdateInfo) -> ApiResult<()> {
553        self.with_conn(|conn| bookmarks::update_bookmark_from_info(conn, item))
554    }
555
556    #[handle_error(crate::Error)]
557    pub fn bookmarks_count_bookmarks_in_trees(&self, guids: &[Guid]) -> ApiResult<u32> {
558        self.with_conn(|conn| bookmarks::count_bookmarks_in_trees(conn, guids))
559    }
560
561    #[handle_error(crate::Error)]
562    pub fn places_history_import_from_ios(
563        &self,
564        db_path: String,
565        last_sync_timestamp: i64,
566    ) -> ApiResult<HistoryMigrationResult> {
567        self.with_conn(|conn| import_ios_history(conn, &db_path, last_sync_timestamp))
568    }
569}
570
571impl AsRef<SqlInterruptHandle> for PlacesConnection {
572    fn as_ref(&self) -> &SqlInterruptHandle {
573        &self.interrupt_handle
574    }
575}
576
577#[derive(Clone, PartialEq, Eq)]
578pub struct HistoryVisitInfo {
579    pub url: Url,
580    pub title: Option<String>,
581    pub timestamp: PlacesTimestamp,
582    pub visit_type: VisitType,
583    pub is_hidden: bool,
584    pub preview_image_url: Option<Url>,
585    pub is_remote: bool,
586}
587#[derive(Clone, PartialEq, Eq)]
588pub struct HistoryVisitInfosWithBound {
589    pub infos: Vec<HistoryVisitInfo>,
590    pub bound: i64,
591    pub offset: i64,
592}
593
594pub struct TopFrecentSiteInfo {
595    pub url: Url,
596    pub title: Option<String>,
597}
598
599pub enum FrecencyThresholdOption {
600    None,
601    SkipOneTimePages,
602}
603
604impl FrecencyThresholdOption {
605    fn value(&self) -> i64 {
606        match self {
607            FrecencyThresholdOption::None => 0,
608            FrecencyThresholdOption::SkipOneTimePages => SKIP_ONE_PAGE_FRECENCY_THRESHOLD,
609        }
610    }
611}
612
613pub struct SearchResult {
614    pub url: Url,
615    pub title: String,
616    pub frecency: i64,
617}
618
619// Exists just to convince uniffi to generate `liftSequence*` helpers!
620pub struct Dummy {
621    pub md: Option<Vec<HistoryMetadata>>,
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::test::new_mem_connection;
628
629    #[test]
630    fn test_accept_result_with_invalid_url() {
631        let conn = PlacesConnection::new(new_mem_connection());
632        let invalid_url = "http://1234.56.78.90".to_string();
633        assert!(PlacesConnection::accept_result(&conn, "ample".to_string(), invalid_url).is_ok());
634    }
635
636    #[test]
637    fn test_bookmarks_get_all_with_url_with_invalid_url() {
638        let conn = PlacesConnection::new(new_mem_connection());
639        let invalid_url = "http://1234.56.78.90".to_string();
640        assert!(PlacesConnection::bookmarks_get_all_with_url(&conn, invalid_url).is_ok());
641    }
642}