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};
33pub use sync_guid::Guid;
34pub use types::Timestamp as PlacesTimestamp;
35pub use url::Url;
36
37const SKIP_ONE_PAGE_FRECENCY_THRESHOLD: i64 = 101 + 1;
39
40pub type InsertableBookmarkItem = crate::storage::bookmarks::InsertableItem;
43pub type InsertableBookmarkFolder = crate::storage::bookmarks::InsertableFolder;
44pub type InsertableBookmarkSeparator = crate::storage::bookmarks::InsertableSeparator;
45pub use crate::storage::bookmarks::InsertableBookmark;
46
47pub use crate::storage::bookmarks::BookmarkUpdateInfo;
48
49pub type BookmarkItem = crate::storage::bookmarks::fetch::Item;
51pub type BookmarkFolder = crate::storage::bookmarks::fetch::Folder;
52pub type BookmarkSeparator = crate::storage::bookmarks::fetch::Separator;
53pub use crate::storage::bookmarks::fetch::BookmarkData;
54
55uniffi::custom_type!(Url, String, {
56 remote,
57 try_lift: |val| {
58 match Url::parse(val.as_str()) {
59 Ok(url) => Ok(url),
60 Err(e) => Err(PlacesApiError::UrlParseFailed {
61 reason: e.to_string(),
62 }
63 .into()),
64 }
65 },
66 lower: |obj| obj.into(),
67});
68
69uniffi::custom_type!(PlacesTimestamp, i64, {
70 remote,
71 try_lift: |val| Ok(PlacesTimestamp(val as u64)),
72 lower: |obj| obj.as_millis() as i64,
73});
74
75uniffi::custom_type!(VisitTransitionSet, i32, {
76 try_lift: |val| {
77 Ok(VisitTransitionSet::from_u16(val as u16).expect("Bug: Invalid VisitTransitionSet"))
78 },
79 lower: |obj| VisitTransitionSet::into_u16(obj) as i32,
80});
81
82uniffi::custom_type!(Guid, String, {
83 remote,
84 try_lift: |val| Ok(Guid::new(val.as_str())),
85 lower: |obj| obj.into(),
86});
87
88lazy_static::lazy_static! {
93 static ref READ_WRITE_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
94 static ref SYNC_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
95}
96
97impl PlacesApi {
98 #[handle_error(crate::Error)]
99 pub fn new_connection(&self, conn_type: ConnectionType) -> ApiResult<Arc<PlacesConnection>> {
100 let db = self.open_connection(conn_type)?;
101 let connection = Arc::new(PlacesConnection::new(db));
102 register_interrupt(Arc::<PlacesConnection>::downgrade(&connection));
103 Ok(connection)
104 }
105}
106
107pub struct PlacesConnection {
108 db: Mutex<PlacesDb>,
109 interrupt_handle: Arc<SqlInterruptHandle>,
110}
111
112impl PlacesConnection {
113 pub fn new(db: PlacesDb) -> Self {
114 Self {
115 interrupt_handle: db.new_interrupt_handle(),
116 db: Mutex::new(db),
117 }
118 }
119
120 fn with_conn<F, T>(&self, f: F) -> Result<T>
122 where
123 F: FnOnce(&PlacesDb) -> crate::error::Result<T>,
124 {
125 let conn = self.db.lock();
126 f(&conn)
127 }
128
129 pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
131 Arc::clone(&self.interrupt_handle)
132 }
133
134 #[handle_error(crate::Error)]
135 pub fn get_latest_history_metadata_for_url(
136 &self,
137 url: Url,
138 ) -> ApiResult<Option<HistoryMetadata>> {
139 self.with_conn(|conn| history_metadata::get_latest_for_url(conn, &url))
140 }
141
142 #[handle_error(crate::Error)]
143 pub fn get_history_metadata_between(
144 &self,
145 start: PlacesTimestamp,
146 end: PlacesTimestamp,
147 ) -> ApiResult<Vec<HistoryMetadata>> {
148 self.with_conn(|conn| {
149 history_metadata::get_between(conn, start.as_millis_i64(), end.as_millis_i64())
150 })
151 }
152
153 #[handle_error(crate::Error)]
154 pub fn get_history_metadata_since(
155 &self,
156 start: PlacesTimestamp,
157 ) -> ApiResult<Vec<HistoryMetadata>> {
158 self.with_conn(|conn| history_metadata::get_since(conn, start.as_millis_i64()))
159 }
160
161 #[handle_error(crate::Error)]
162 pub fn get_most_recent_history_metadata(&self, limit: i32) -> ApiResult<Vec<HistoryMetadata>> {
163 self.with_conn(|conn| history_metadata::get_most_recent(conn, limit))
164 }
165
166 #[handle_error(crate::Error)]
167 pub fn get_most_recent_search_entries_in_history_metadata(
168 &self,
169 limit: i32,
170 ) -> ApiResult<Vec<HistoryMetadata>> {
171 self.with_conn(|conn| history_metadata::get_most_recent_search_entries(conn, limit))
172 }
173
174 #[handle_error(crate::Error)]
175 pub fn query_history_metadata(
176 &self,
177 query: String,
178 limit: i32,
179 ) -> ApiResult<Vec<HistoryMetadata>> {
180 self.with_conn(|conn| history_metadata::query(conn, query.as_str(), limit))
181 }
182
183 #[handle_error(crate::Error)]
184 pub fn get_history_highlights(
185 &self,
186 weights: HistoryHighlightWeights,
187 limit: i32,
188 ) -> ApiResult<Vec<HistoryHighlight>> {
189 self.with_conn(|conn| history_metadata::get_highlights(conn, weights, limit))
190 }
191
192 #[handle_error(crate::Error)]
193 pub fn note_history_metadata_observation(
194 &self,
195 data: HistoryMetadataObservation,
196 options: NoteHistoryMetadataObservationOptions,
197 ) -> ApiResult<()> {
198 self.with_conn(|conn| history_metadata::apply_metadata_observation(conn, data, options))
200 }
201
202 #[handle_error(crate::Error)]
203 pub fn metadata_delete_older_than(&self, older_than: PlacesTimestamp) -> ApiResult<()> {
204 self.with_conn(|conn| history_metadata::delete_older_than(conn, older_than.as_millis_i64()))
205 }
206
207 #[handle_error(crate::Error)]
208 pub fn metadata_delete(
209 &self,
210 url: Url,
211 referrer_url: Option<Url>,
212 search_term: Option<String>,
213 ) -> ApiResult<()> {
214 self.with_conn(|conn| {
215 history_metadata::delete_metadata(
216 conn,
217 &url,
218 referrer_url.as_ref(),
219 search_term.as_deref(),
220 )
221 })
222 }
223
224 #[handle_error(crate::Error)]
225 pub fn metadata_delete_search_terms(&self) -> ApiResult<()> {
226 self.with_conn(history_metadata::delete_all_metadata_for_search)
227 }
228
229 #[handle_error(crate::Error)]
231 pub fn apply_observation(&self, visit: VisitObservation) -> ApiResult<()> {
232 self.with_conn(|conn| history::apply_observation(conn, visit))?;
233 Ok(())
234 }
235
236 #[handle_error(crate::Error)]
237 pub fn get_visited_urls_in_range(
238 &self,
239 start: PlacesTimestamp,
240 end: PlacesTimestamp,
241 include_remote: bool,
242 ) -> ApiResult<Vec<Url>> {
243 self.with_conn(|conn| {
244 let urls = history::get_visited_urls(conn, start, end, include_remote)?
245 .iter()
246 .filter_map(|s| Url::parse(s).ok())
248 .collect::<Vec<_>>();
249 Ok(urls)
250 })
251 }
252
253 #[handle_error(crate::Error)]
254 pub fn get_visit_infos(
255 &self,
256 start_date: PlacesTimestamp,
257 end_date: PlacesTimestamp,
258 exclude_types: VisitTransitionSet,
259 ) -> ApiResult<Vec<HistoryVisitInfo>> {
260 self.with_conn(|conn| history::get_visit_infos(conn, start_date, end_date, exclude_types))
261 }
262
263 #[handle_error(crate::Error)]
264 pub fn get_visit_count(&self, exclude_types: VisitTransitionSet) -> ApiResult<i64> {
265 self.with_conn(|conn| history::get_visit_count(conn, exclude_types))
266 }
267
268 #[handle_error(crate::Error)]
269 pub fn get_visit_count_for_host(
270 &self,
271 host: String,
272 before: PlacesTimestamp,
273 exclude_types: VisitTransitionSet,
274 ) -> ApiResult<i64> {
275 self.with_conn(|conn| {
276 history::get_visit_count_for_host(conn, host.as_str(), before, exclude_types)
277 })
278 }
279
280 #[handle_error(crate::Error)]
281 pub fn get_visit_page(
282 &self,
283 offset: i64,
284 count: i64,
285 exclude_types: VisitTransitionSet,
286 ) -> ApiResult<Vec<HistoryVisitInfo>> {
287 self.with_conn(|conn| history::get_visit_page(conn, offset, count, exclude_types))
288 }
289
290 #[handle_error(crate::Error)]
291 pub fn get_visit_page_with_bound(
292 &self,
293 bound: i64,
294 offset: i64,
295 count: i64,
296 exclude_types: VisitTransitionSet,
297 ) -> ApiResult<HistoryVisitInfosWithBound> {
298 self.with_conn(|conn| {
299 history::get_visit_page_with_bound(conn, bound, offset, count, exclude_types)
300 })
301 }
302
303 #[handle_error(crate::Error)]
307 pub fn get_visited(&self, urls: Vec<String>) -> ApiResult<Vec<bool>> {
308 let iter = urls.into_iter();
309 let mut result = vec![false; iter.len()];
310 let url_idxs = iter
311 .enumerate()
312 .filter_map(|(idx, s)| Url::parse(&s).ok().map(|url| (idx, url)))
313 .collect::<Vec<_>>();
314 self.with_conn(|conn| history::get_visited_into(conn, &url_idxs, &mut result))?;
315 Ok(result)
316 }
317
318 #[handle_error(crate::Error)]
319 pub fn delete_visits_for(&self, url: String) -> ApiResult<()> {
320 self.with_conn(|conn| {
321 let guid = match Url::parse(&url) {
322 Ok(url) => history::url_to_guid(conn, &url)?,
323 Err(e) => {
324 warn!("Invalid URL passed to places_delete_visits_for, {}", e);
325 history::href_to_guid(conn, url.clone().as_str())?
326 }
327 };
328 if let Some(guid) = guid {
329 history::delete_visits_for(conn, &guid)?;
330 }
331 Ok(())
332 })
333 }
334
335 #[handle_error(crate::Error)]
336 pub fn delete_visits_between(
337 &self,
338 start: PlacesTimestamp,
339 end: PlacesTimestamp,
340 ) -> ApiResult<()> {
341 self.with_conn(|conn| history::delete_visits_between(conn, start, end))
342 }
343
344 #[handle_error(crate::Error)]
345 pub fn delete_visit(&self, url: String, timestamp: PlacesTimestamp) -> ApiResult<()> {
346 self.with_conn(|conn| {
347 match Url::parse(&url) {
348 Ok(url) => {
349 history::delete_place_visit_at_time(conn, &url, timestamp)?;
350 }
351 Err(e) => {
352 warn!("Invalid URL passed to places_delete_visit, {}", e);
353 history::delete_place_visit_at_time_by_href(conn, url.as_str(), timestamp)?;
354 }
355 };
356 Ok(())
357 })
358 }
359
360 #[handle_error(crate::Error)]
361 pub fn get_top_frecent_site_infos(
362 &self,
363 num_items: i32,
364 threshold_option: FrecencyThresholdOption,
365 ) -> ApiResult<Vec<TopFrecentSiteInfo>> {
366 self.with_conn(|conn| {
367 crate::storage::history::get_top_frecent_site_infos(
368 conn,
369 num_items,
370 threshold_option.value(),
371 )
372 })
373 }
374 #[handle_error(crate::Error)]
377 pub fn delete_everything_history(&self) -> ApiResult<()> {
378 history::delete_everything(&self.db.lock())
379 }
380
381 #[handle_error(crate::Error)]
382 pub fn run_maintenance_prune(
383 &self,
384 db_size_limit: u32,
385 prune_limit: u32,
386 ) -> ApiResult<RunMaintenanceMetrics> {
387 self.with_conn(|conn| storage::run_maintenance_prune(conn, db_size_limit, prune_limit))
388 }
389
390 #[handle_error(crate::Error)]
391 pub fn run_maintenance_vacuum(&self) -> ApiResult<()> {
392 self.with_conn(storage::run_maintenance_vacuum)
393 }
394
395 #[handle_error(crate::Error)]
396 pub fn run_maintenance_optimize(&self) -> ApiResult<()> {
397 self.with_conn(storage::run_maintenance_optimize)
398 }
399
400 #[handle_error(crate::Error)]
401 pub fn run_maintenance_checkpoint(&self) -> ApiResult<()> {
402 self.with_conn(storage::run_maintenance_checkpoint)
403 }
404
405 #[handle_error(crate::Error)]
406 pub fn query_autocomplete(&self, search: String, limit: i32) -> ApiResult<Vec<SearchResult>> {
407 self.with_conn(|conn| {
408 search_frecent(
409 conn,
410 SearchParams {
411 search_string: search,
412 limit: limit as u32,
413 },
414 )
415 .map(|search_results| search_results.into_iter().map(Into::into).collect())
416 })
417 }
418
419 #[handle_error(crate::Error)]
420 pub fn accept_result(&self, search_string: String, url: String) -> ApiResult<()> {
421 self.with_conn(|conn| {
422 match Url::parse(&url) {
423 Ok(url) => {
424 matcher::accept_result(conn, &search_string, &url)?;
425 }
426 Err(_) => {
427 warn!("Ignoring invalid URL in places_accept_result");
428 return Ok(());
429 }
430 };
431 Ok(())
432 })
433 }
434
435 #[handle_error(crate::Error)]
436 pub fn match_url(&self, query: String) -> ApiResult<Option<Url>> {
437 self.with_conn(|conn| matcher::match_url(conn, query))
438 }
439
440 #[handle_error(crate::Error)]
441 pub fn bookmarks_get_tree(&self, item_guid: &Guid) -> ApiResult<Option<BookmarkItem>> {
442 self.with_conn(|conn| bookmarks::fetch::fetch_tree(conn, item_guid))
443 }
444
445 #[handle_error(crate::Error)]
446 pub fn bookmarks_get_by_guid(
447 &self,
448 guid: &Guid,
449 get_direct_children: bool,
450 ) -> ApiResult<Option<BookmarkItem>> {
451 self.with_conn(|conn| {
452 let bookmark = bookmarks::fetch::fetch_bookmark(conn, guid, get_direct_children)?;
453 Ok(bookmark)
454 })
455 }
456
457 #[handle_error(crate::Error)]
458 pub fn bookmarks_get_all_with_url(&self, url: String) -> ApiResult<Vec<BookmarkItem>> {
459 self.with_conn(|conn| {
460 match Url::parse(&url) {
462 Ok(url) => Ok(bookmarks::fetch::fetch_bookmarks_by_url(conn, &url)?
463 .into_iter()
464 .map(|b| BookmarkItem::Bookmark { b })
465 .collect::<Vec<BookmarkItem>>()),
466 Err(e) => {
467 warn!("Invalid URL passed to bookmarks_get_all_with_url, {}", e);
469 Ok(Vec::<BookmarkItem>::new())
470 }
471 }
472 })
473 }
474
475 #[handle_error(crate::Error)]
476 pub fn bookmarks_search(&self, query: String, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
477 self.with_conn(|conn| {
478 Ok(
480 bookmarks::fetch::search_bookmarks(conn, query.as_str(), limit as u32)?
481 .into_iter()
482 .map(|b| BookmarkItem::Bookmark { b })
483 .collect(),
484 )
485 })
486 }
487
488 #[handle_error(crate::Error)]
489 pub fn bookmarks_get_recent(&self, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
490 self.with_conn(|conn| {
491 Ok(bookmarks::fetch::recent_bookmarks(conn, limit as u32)?
493 .into_iter()
494 .map(|b| BookmarkItem::Bookmark { b })
495 .collect())
496 })
497 }
498
499 #[handle_error(crate::Error)]
500 pub fn bookmarks_delete(&self, id: Guid) -> ApiResult<bool> {
501 self.with_conn(|conn| bookmarks::delete_bookmark(conn, &id))
502 }
503
504 #[handle_error(crate::Error)]
505 pub fn bookmarks_delete_everything(&self) -> ApiResult<()> {
506 self.with_conn(bookmarks::delete_everything)
507 }
508
509 #[handle_error(crate::Error)]
510 pub fn bookmarks_get_url_for_keyword(&self, keyword: String) -> ApiResult<Option<Url>> {
511 self.with_conn(|conn| bookmarks::bookmarks_get_url_for_keyword(conn, keyword.as_str()))
512 }
513
514 #[handle_error(crate::Error)]
515 pub fn bookmarks_insert(&self, data: InsertableBookmarkItem) -> ApiResult<Guid> {
516 self.with_conn(|conn| bookmarks::insert_bookmark(conn, data))
517 }
518
519 #[handle_error(crate::Error)]
520 pub fn bookmarks_update(&self, item: BookmarkUpdateInfo) -> ApiResult<()> {
521 self.with_conn(|conn| bookmarks::update_bookmark_from_info(conn, item))
522 }
523
524 #[handle_error(crate::Error)]
525 pub fn bookmarks_count_bookmarks_in_trees(&self, guids: &[Guid]) -> ApiResult<u32> {
526 self.with_conn(|conn| bookmarks::count_bookmarks_in_trees(conn, guids))
527 }
528
529 #[handle_error(crate::Error)]
530 pub fn places_history_import_from_ios(
531 &self,
532 db_path: String,
533 last_sync_timestamp: i64,
534 ) -> ApiResult<HistoryMigrationResult> {
535 self.with_conn(|conn| import_ios_history(conn, &db_path, last_sync_timestamp))
536 }
537}
538
539impl AsRef<SqlInterruptHandle> for PlacesConnection {
540 fn as_ref(&self) -> &SqlInterruptHandle {
541 &self.interrupt_handle
542 }
543}
544
545#[derive(Clone, PartialEq, Eq)]
546pub struct HistoryVisitInfo {
547 pub url: Url,
548 pub title: Option<String>,
549 pub timestamp: PlacesTimestamp,
550 pub visit_type: VisitType,
551 pub is_hidden: bool,
552 pub preview_image_url: Option<Url>,
553 pub is_remote: bool,
554}
555#[derive(Clone, PartialEq, Eq)]
556pub struct HistoryVisitInfosWithBound {
557 pub infos: Vec<HistoryVisitInfo>,
558 pub bound: i64,
559 pub offset: i64,
560}
561
562pub struct TopFrecentSiteInfo {
563 pub url: Url,
564 pub title: Option<String>,
565}
566
567pub enum FrecencyThresholdOption {
568 None,
569 SkipOneTimePages,
570}
571
572impl FrecencyThresholdOption {
573 fn value(&self) -> i64 {
574 match self {
575 FrecencyThresholdOption::None => 0,
576 FrecencyThresholdOption::SkipOneTimePages => SKIP_ONE_PAGE_FRECENCY_THRESHOLD,
577 }
578 }
579}
580
581pub struct SearchResult {
582 pub url: Url,
583 pub title: String,
584 pub frecency: i64,
585}
586
587pub struct Dummy {
589 pub md: Option<Vec<HistoryMetadata>>,
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::test::new_mem_connection;
596
597 #[test]
598 fn test_accept_result_with_invalid_url() {
599 let conn = PlacesConnection::new(new_mem_connection());
600 let invalid_url = "http://1234.56.78.90".to_string();
601 assert!(PlacesConnection::accept_result(&conn, "ample".to_string(), invalid_url).is_ok());
602 }
603
604 #[test]
605 fn test_bookmarks_get_all_with_url_with_invalid_url() {
606 let conn = PlacesConnection::new(new_mem_connection());
607 let invalid_url = "http://1234.56.78.90".to_string();
608 assert!(PlacesConnection::bookmarks_get_all_with_url(&conn, invalid_url).is_ok());
609 }
610}