1use 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
38const SKIP_ONE_PAGE_FRECENCY_THRESHOLD: i64 = 101 + 1;
40
41pub 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
50pub 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
89lazy_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 #[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 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 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 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 #[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 .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 #[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 #[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 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 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 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 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
619pub 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}