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