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 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 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 #[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 .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 #[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 #[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 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 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 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 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
624pub 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}