1use super::RowId;
6use super::{delete_meta, put_meta};
7use super::{fetch_page_info, new_page_info};
8use crate::bookmark_sync::engine::{
9 COLLECTION_SYNCID_META_KEY, GLOBAL_SYNCID_META_KEY, LAST_SYNC_META_KEY,
10};
11use crate::db::PlacesDb;
12use crate::error::*;
13use crate::types::{BookmarkType, SyncStatus};
14use rusqlite::{self, Connection, Row};
15#[cfg(test)]
16use serde_json::{self, json};
17use sql_support::{self, repeat_sql_vars, ConnExt};
18use std::cmp::{max, min};
19use sync15::engine::EngineSyncAssociation;
20use sync_guid::Guid as SyncGuid;
21use types::Timestamp;
22use url::Url;
23
24pub use root_guid::{BookmarkRootGuid, USER_CONTENT_ROOTS};
25
26mod conversions;
27pub mod fetch;
28pub mod json_tree;
29mod root_guid;
30
31fn create_root(
32 db: &Connection,
33 title: &str,
34 guid: &SyncGuid,
35 position: u32,
36 when: Timestamp,
37) -> rusqlite::Result<()> {
38 let sql = format!(
39 "
40 INSERT INTO moz_bookmarks
41 (type, position, title, dateAdded, lastModified, guid, parent,
42 syncChangeCounter, syncStatus)
43 VALUES
44 (:item_type, :item_position, :item_title, :date_added, :last_modified, :guid,
45 (SELECT id FROM moz_bookmarks WHERE guid = {:?}),
46 1, :sync_status)
47 ",
48 BookmarkRootGuid::Root.as_guid().as_str()
49 );
50 let params = rusqlite::named_params! {
51 ":item_type": &BookmarkType::Folder,
52 ":item_position": &position,
53 ":item_title": &title,
54 ":date_added": &when,
55 ":last_modified": &when,
56 ":guid": guid,
57 ":sync_status": &SyncStatus::New,
58 };
59 db.execute_cached(&sql, params)?;
60 Ok(())
61}
62
63pub fn create_bookmark_roots(db: &Connection) -> rusqlite::Result<()> {
64 let now = Timestamp::now();
65 create_root(db, "root", &BookmarkRootGuid::Root.into(), 0, now)?;
66 create_root(db, "menu", &BookmarkRootGuid::Menu.into(), 0, now)?;
67 create_root(db, "toolbar", &BookmarkRootGuid::Toolbar.into(), 1, now)?;
68 create_root(db, "unfiled", &BookmarkRootGuid::Unfiled.into(), 2, now)?;
69 create_root(db, "mobile", &BookmarkRootGuid::Mobile.into(), 3, now)?;
70 Ok(())
71}
72
73#[derive(Debug, Copy, Clone)]
74pub enum BookmarkPosition {
75 Specific { pos: u32 },
76 Append,
77}
78
79fn resolve_pos_for_insert(
85 db: &PlacesDb,
86 pos: BookmarkPosition,
87 parent: &RawBookmark,
88) -> Result<u32> {
89 Ok(match pos {
90 BookmarkPosition::Specific { pos } => {
91 let actual = min(pos, parent.child_count);
92 db.execute_cached(
94 "UPDATE moz_bookmarks SET position = position + 1
95 WHERE parent = :parent_id
96 AND position >= :position",
97 &[
98 (":parent_id", &parent.row_id as &dyn rusqlite::ToSql),
99 (":position", &actual),
100 ],
101 )?;
102 actual
103 }
104 BookmarkPosition::Append => parent.child_count,
105 })
106}
107
108fn update_pos_for_deletion(db: &PlacesDb, pos: u32, parent_id: RowId) -> Result<()> {
111 db.execute_cached(
112 "UPDATE moz_bookmarks SET position = position - 1
113 WHERE parent = :parent
114 AND position >= :position",
115 &[
116 (":parent", &parent_id as &dyn rusqlite::ToSql),
117 (":position", &pos),
118 ],
119 )?;
120 Ok(())
121}
122
123fn update_pos_for_move(
127 db: &PlacesDb,
128 pos: BookmarkPosition,
129 bm: &RawBookmark,
130 parent: &RawBookmark,
131) -> Result<u32> {
132 assert_eq!(bm.parent_id.unwrap(), parent.row_id);
133 let new_index = match pos {
136 BookmarkPosition::Specific { pos } => min(pos, parent.child_count - 1),
137 BookmarkPosition::Append => parent.child_count - 1,
138 };
139 db.execute_cached(
140 "UPDATE moz_bookmarks
141 SET position = CASE WHEN :new_index < :cur_index
142 THEN position + 1
143 ELSE position - 1
144 END
145 WHERE parent = :parent_id
146 AND position BETWEEN :low_index AND :high_index",
147 &[
148 (":new_index", &new_index as &dyn rusqlite::ToSql),
149 (":cur_index", &bm.position),
150 (":parent_id", &parent.row_id),
151 (":low_index", &min(bm.position, new_index)),
152 (":high_index", &max(bm.position, new_index)),
153 ],
154 )?;
155 Ok(new_index)
156}
157
158#[derive(Debug, Clone)]
160pub struct InsertableBookmark {
161 pub parent_guid: SyncGuid,
162 pub position: BookmarkPosition,
163 pub date_added: Option<Timestamp>,
164 pub last_modified: Option<Timestamp>,
165 pub guid: Option<SyncGuid>,
166 pub url: Url,
167 pub title: Option<String>,
168}
169
170impl From<InsertableBookmark> for InsertableItem {
171 fn from(b: InsertableBookmark) -> Self {
172 InsertableItem::Bookmark { b }
173 }
174}
175
176#[derive(Debug, Clone)]
177pub struct InsertableSeparator {
178 pub parent_guid: SyncGuid,
179 pub position: BookmarkPosition,
180 pub date_added: Option<Timestamp>,
181 pub last_modified: Option<Timestamp>,
182 pub guid: Option<SyncGuid>,
183}
184
185impl From<InsertableSeparator> for InsertableItem {
186 fn from(s: InsertableSeparator) -> Self {
187 InsertableItem::Separator { s }
188 }
189}
190
191#[derive(Debug, Clone)]
192pub struct InsertableFolder {
193 pub parent_guid: SyncGuid,
194 pub position: BookmarkPosition,
195 pub date_added: Option<Timestamp>,
196 pub last_modified: Option<Timestamp>,
197 pub guid: Option<SyncGuid>,
198 pub title: Option<String>,
199 pub children: Vec<InsertableItem>,
200}
201
202impl From<InsertableFolder> for InsertableItem {
203 fn from(f: InsertableFolder) -> Self {
204 InsertableItem::Folder { f }
205 }
206}
207
208#[derive(Debug, Clone)]
210pub enum InsertableItem {
211 Bookmark { b: InsertableBookmark },
212 Separator { s: InsertableSeparator },
213 Folder { f: InsertableFolder },
214}
215
216macro_rules! impl_common_bookmark_getter {
219 ($getter_name:ident, $T:ty) => {
220 fn $getter_name(&self) -> &$T {
221 match self {
222 InsertableItem::Bookmark { b } => &b.$getter_name,
223 InsertableItem::Separator { s } => &s.$getter_name,
224 InsertableItem::Folder { f } => &f.$getter_name,
225 }
226 }
227 };
228}
229
230impl InsertableItem {
231 fn bookmark_type(&self) -> BookmarkType {
232 match self {
233 InsertableItem::Bookmark { .. } => BookmarkType::Bookmark,
234 InsertableItem::Separator { .. } => BookmarkType::Separator,
235 InsertableItem::Folder { .. } => BookmarkType::Folder,
236 }
237 }
238 impl_common_bookmark_getter!(parent_guid, SyncGuid);
239 impl_common_bookmark_getter!(position, BookmarkPosition);
240 impl_common_bookmark_getter!(date_added, Option<Timestamp>);
241 impl_common_bookmark_getter!(last_modified, Option<Timestamp>);
242 impl_common_bookmark_getter!(guid, Option<SyncGuid>);
243
244 fn set_parent_guid(&mut self, guid: SyncGuid) {
246 match self {
247 InsertableItem::Bookmark { b } => b.parent_guid = guid,
248 InsertableItem::Separator { s } => s.parent_guid = guid,
249 InsertableItem::Folder { f } => f.parent_guid = guid,
250 }
251 }
252
253 fn set_last_modified(&mut self, ts: Timestamp) {
254 match self {
255 InsertableItem::Bookmark { b } => b.last_modified = Some(ts),
256 InsertableItem::Separator { s } => s.last_modified = Some(ts),
257 InsertableItem::Folder { f } => f.last_modified = Some(ts),
258 }
259 }
260
261 fn set_date_added(&mut self, ts: Timestamp) {
262 match self {
263 InsertableItem::Bookmark { b } => b.date_added = Some(ts),
264 InsertableItem::Separator { s } => s.date_added = Some(ts),
265 InsertableItem::Folder { f } => f.date_added = Some(ts),
266 }
267 }
268}
269
270pub fn insert_bookmark(db: &PlacesDb, bm: InsertableItem) -> Result<SyncGuid> {
271 let tx = db.begin_transaction()?;
272 let result = insert_bookmark_in_tx(db, bm);
273 super::delete_pending_temp_tables(db)?;
274 match result {
275 Ok(_) => tx.commit()?,
276 Err(_) => tx.rollback()?,
277 }
278 result
279}
280
281pub fn maybe_truncate_title<'a>(t: &Option<&'a str>) -> Option<&'a str> {
282 use super::TITLE_LENGTH_MAX;
283 use crate::util::slice_up_to;
284 t.map(|title| slice_up_to(title, TITLE_LENGTH_MAX))
285}
286
287fn insert_bookmark_in_tx(db: &PlacesDb, bm: InsertableItem) -> Result<SyncGuid> {
288 if bm.parent_guid() == BookmarkRootGuid::Root {
290 return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
291 }
292 let parent_guid = bm.parent_guid();
293 let parent = get_raw_bookmark(db, parent_guid)?
294 .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(parent_guid.to_string()))?;
295 if parent.bookmark_type != BookmarkType::Folder {
296 return Err(InvalidPlaceInfo::InvalidParent(parent_guid.to_string()).into());
297 }
298 let position = resolve_pos_for_insert(db, *bm.position(), &parent)?;
300
301 let fk = match bm {
305 InsertableItem::Bookmark { ref b } => {
306 let page_info = match fetch_page_info(db, &b.url)? {
307 Some(info) => info.page,
308 None => new_page_info(db, &b.url, None)?,
309 };
310 Some(page_info.row_id)
311 }
312 _ => None,
313 };
314 let sql = "INSERT INTO moz_bookmarks
315 (fk, type, parent, position, title, dateAdded, lastModified,
316 guid, syncStatus, syncChangeCounter) VALUES
317 (:fk, :type, :parent, :position, :title, :dateAdded, :lastModified,
318 :guid, :syncStatus, :syncChangeCounter)";
319
320 let guid = bm.guid().clone().unwrap_or_else(SyncGuid::random);
321 if !guid.is_valid_for_places() || !guid.is_valid_for_sync_server() {
322 return Err(InvalidPlaceInfo::InvalidGuid.into());
323 }
324 let date_added = bm.date_added().unwrap_or_else(Timestamp::now);
325 let last_modified = max(
327 bm.last_modified().unwrap_or_else(Timestamp::now),
328 date_added,
329 );
330
331 let bookmark_type = bm.bookmark_type();
332 match bm {
333 InsertableItem::Bookmark { ref b } => {
334 let title = maybe_truncate_title(&b.title.as_deref());
335 db.execute_cached(
336 sql,
337 &[
338 (":fk", &fk as &dyn rusqlite::ToSql),
339 (":type", &bookmark_type),
340 (":parent", &parent.row_id),
341 (":position", &position),
342 (":title", &title),
343 (":dateAdded", &date_added),
344 (":lastModified", &last_modified),
345 (":guid", &guid),
346 (":syncStatus", &SyncStatus::New),
347 (":syncChangeCounter", &1),
348 ],
349 )?;
350 }
351 InsertableItem::Separator { .. } => {
352 db.execute_cached(
353 sql,
354 &[
355 (":type", &bookmark_type as &dyn rusqlite::ToSql),
356 (":parent", &parent.row_id),
357 (":position", &position),
358 (":dateAdded", &date_added),
359 (":lastModified", &last_modified),
360 (":guid", &guid),
361 (":syncStatus", &SyncStatus::New),
362 (":syncChangeCounter", &1),
363 ],
364 )?;
365 }
366 InsertableItem::Folder { f } => {
367 let title = maybe_truncate_title(&f.title.as_deref());
368 db.execute_cached(
369 sql,
370 &[
371 (":type", &bookmark_type as &dyn rusqlite::ToSql),
372 (":parent", &parent.row_id),
373 (":title", &title),
374 (":position", &position),
375 (":dateAdded", &date_added),
376 (":lastModified", &last_modified),
377 (":guid", &guid),
378 (":syncStatus", &SyncStatus::New),
379 (":syncChangeCounter", &1),
380 ],
381 )?;
382 for mut child in f.children.into_iter() {
384 let specified_parent_guid = child.parent_guid();
387 if specified_parent_guid.is_empty() {
388 child.set_parent_guid(guid.clone());
389 } else if *specified_parent_guid != guid {
390 return Err(
391 InvalidPlaceInfo::InvalidParent(specified_parent_guid.to_string()).into(),
392 );
393 }
394 if child.last_modified().is_none() {
396 child.set_last_modified(last_modified);
397 }
398 if child.date_added().is_none() {
399 child.set_date_added(date_added);
400 }
401 insert_bookmark_in_tx(db, child)?;
402 }
403 }
404 };
405
406 let sql_counter = "
408 UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1
409 WHERE id = :parent_id";
410 db.execute_cached(sql_counter, &[(":parent_id", &parent.row_id)])?;
411
412 Ok(guid)
413}
414
415pub fn delete_bookmark(db: &PlacesDb, guid: &SyncGuid) -> Result<bool> {
418 let tx = db.begin_transaction()?;
419 let result = delete_bookmark_in_tx(db, guid);
420 match result {
421 Ok(_) => tx.commit()?,
422 Err(_) => tx.rollback()?,
423 }
424 result
425}
426
427fn delete_bookmark_in_tx(db: &PlacesDb, guid: &SyncGuid) -> Result<bool> {
428 if let Some(root) = BookmarkRootGuid::well_known(guid.as_str()) {
430 return Err(InvalidPlaceInfo::CannotUpdateRoot(root).into());
431 }
432 let record = match get_raw_bookmark(db, guid)? {
433 Some(r) => r,
434 None => {
435 debug!("Can't delete bookmark '{:?}' as it doesn't exist", guid);
436 return Ok(false);
437 }
438 };
439 let record_parent_id = record
443 .parent_id
444 .ok_or_else(|| Corruption::NonRootWithoutParent(guid.to_string()))?;
445 update_pos_for_deletion(db, record.position, record_parent_id)?;
447 db.execute_cached(
449 "DELETE from moz_bookmarks WHERE id = :id",
450 &[(":id", &record.row_id)],
451 )?;
452 super::delete_pending_temp_tables(db)?;
453 Ok(true)
454}
455
456#[derive(Debug, Default, Clone)]
461pub enum UpdateTreeLocation {
462 #[default]
463 None, Position {
465 pos: BookmarkPosition,
466 }, Parent {
468 guid: SyncGuid,
469 pos: BookmarkPosition,
470 }, }
472
473#[derive(Debug, Clone, Default)]
478pub struct UpdatableBookmark {
479 pub location: UpdateTreeLocation,
480 pub url: Option<Url>,
481 pub title: Option<String>,
482}
483
484impl From<UpdatableBookmark> for UpdatableItem {
485 fn from(b: UpdatableBookmark) -> Self {
486 UpdatableItem::Bookmark { b }
487 }
488}
489
490#[derive(Debug, Clone)]
491pub struct UpdatableSeparator {
492 pub location: UpdateTreeLocation,
493}
494
495impl From<UpdatableSeparator> for UpdatableItem {
496 fn from(s: UpdatableSeparator) -> Self {
497 UpdatableItem::Separator { s }
498 }
499}
500
501#[derive(Debug, Clone, Default)]
502pub struct UpdatableFolder {
503 pub location: UpdateTreeLocation,
504 pub title: Option<String>,
505}
506
507impl From<UpdatableFolder> for UpdatableItem {
508 fn from(f: UpdatableFolder) -> Self {
509 UpdatableItem::Folder { f }
510 }
511}
512
513#[derive(Debug, Clone)]
515pub enum UpdatableItem {
516 Bookmark { b: UpdatableBookmark },
517 Separator { s: UpdatableSeparator },
518 Folder { f: UpdatableFolder },
519}
520
521impl UpdatableItem {
522 fn bookmark_type(&self) -> BookmarkType {
523 match self {
524 UpdatableItem::Bookmark { .. } => BookmarkType::Bookmark,
525 UpdatableItem::Separator { .. } => BookmarkType::Separator,
526 UpdatableItem::Folder { .. } => BookmarkType::Folder,
527 }
528 }
529
530 pub fn location(&self) -> &UpdateTreeLocation {
531 match self {
532 UpdatableItem::Bookmark { b } => &b.location,
533 UpdatableItem::Separator { s } => &s.location,
534 UpdatableItem::Folder { f } => &f.location,
535 }
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Eq)]
554pub struct BookmarkUpdateInfo {
555 pub guid: SyncGuid,
556 pub title: Option<String>,
557 pub url: Option<String>,
558 pub parent_guid: Option<SyncGuid>,
559 pub position: Option<u32>,
560}
561
562pub fn update_bookmark_from_info(db: &PlacesDb, info: BookmarkUpdateInfo) -> Result<()> {
563 let tx = db.begin_transaction()?;
564 let existing = get_raw_bookmark(db, &info.guid)?
565 .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(info.guid.to_string()))?;
566 let (guid, updatable) = info.into_updatable(existing.bookmark_type)?;
567
568 update_bookmark_in_tx(db, &guid, &updatable, existing)?;
569 tx.commit()?;
570 Ok(())
571}
572
573pub fn update_bookmark(db: &PlacesDb, guid: &SyncGuid, item: &UpdatableItem) -> Result<()> {
574 let tx = db.begin_transaction()?;
575 let existing = get_raw_bookmark(db, guid)?
576 .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(guid.to_string()))?;
577 let result = update_bookmark_in_tx(db, guid, item, existing);
578 super::delete_pending_temp_tables(db)?;
579 tx.commit()?;
581 result
582}
583
584fn update_bookmark_in_tx(
585 db: &PlacesDb,
586 guid: &SyncGuid,
587 item: &UpdatableItem,
588 raw: RawBookmark,
589) -> Result<()> {
590 if BookmarkRootGuid::well_known(guid.as_str()).is_some() {
592 return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
593 }
594 let existing_parent_guid = raw
595 .parent_guid
596 .as_ref()
597 .ok_or_else(|| Corruption::NonRootWithoutParent(guid.to_string()))?;
598
599 let existing_parent_id = raw
600 .parent_id
601 .ok_or_else(|| Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string()))?;
602
603 if raw.bookmark_type != item.bookmark_type() {
604 return Err(InvalidPlaceInfo::MismatchedBookmarkType(
605 raw.bookmark_type as u8,
606 item.bookmark_type() as u8,
607 )
608 .into());
609 }
610
611 let update_old_parent_status;
612 let update_new_parent_status;
613 let parent_id;
616 let position;
617 match item.location() {
618 UpdateTreeLocation::None => {
619 parent_id = existing_parent_id;
620 position = raw.position;
621 update_old_parent_status = false;
622 update_new_parent_status = false;
623 }
624 UpdateTreeLocation::Position { pos } => {
625 parent_id = existing_parent_id;
626 update_old_parent_status = true;
627 update_new_parent_status = false;
628 let parent = get_raw_bookmark(db, existing_parent_guid)?.ok_or_else(|| {
629 Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string())
630 })?;
631 position = update_pos_for_move(db, *pos, &raw, &parent)?;
632 }
633 UpdateTreeLocation::Parent {
634 guid: new_parent_guid,
635 pos,
636 } => {
637 if new_parent_guid == BookmarkRootGuid::Root {
638 return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
639 }
640 let new_parent = get_raw_bookmark(db, new_parent_guid)?
641 .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(new_parent_guid.to_string()))?;
642 if new_parent.bookmark_type != BookmarkType::Folder {
643 return Err(InvalidPlaceInfo::InvalidParent(new_parent_guid.to_string()).into());
644 }
645 parent_id = new_parent.row_id;
646 update_old_parent_status = true;
647 update_new_parent_status = true;
648 let existing_parent = get_raw_bookmark(db, existing_parent_guid)?.ok_or_else(|| {
649 Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string())
650 })?;
651 update_pos_for_deletion(db, raw.position, existing_parent.row_id)?;
652 position = resolve_pos_for_insert(db, *pos, &new_parent)?;
653 }
654 };
655 let place_id = match item {
656 UpdatableItem::Bookmark { b } => match &b.url {
657 None => raw.place_id,
658 Some(url) => {
659 let page_info = match fetch_page_info(db, url)? {
660 Some(info) => info.page,
661 None => new_page_info(db, url, None)?,
662 };
663 Some(page_info.row_id)
664 }
665 },
666 _ => {
667 assert_eq!(raw.place_id, None);
670 None
671 }
672 };
673 let update_title = match item {
677 UpdatableItem::Bookmark { b } => &b.title,
678 UpdatableItem::Folder { f } => &f.title,
679 UpdatableItem::Separator { .. } => &None,
680 };
681
682 let title: Option<String> = match update_title {
683 None => raw.title.clone(),
684 Some(val) => {
688 if val.is_empty() {
689 None
690 } else {
691 Some(val.clone())
692 }
693 }
694 };
695
696 let change_incr = title != raw.title || place_id != raw.place_id;
697
698 let now = Timestamp::now();
699
700 let sql = "
701 UPDATE moz_bookmarks SET
702 fk = :fk,
703 parent = :parent,
704 position = :position,
705 title = :title,
706 lastModified = :now,
707 syncChangeCounter = syncChangeCounter + :change_incr
708 WHERE id = :id";
709
710 db.execute_cached(
711 sql,
712 &[
713 (":fk", &place_id as &dyn rusqlite::ToSql),
714 (":parent", &parent_id),
715 (":position", &position),
716 (":title", &maybe_truncate_title(&title.as_deref())),
717 (":now", &now),
718 (":change_incr", &(change_incr as u32)),
719 (":id", &raw.row_id),
720 ],
721 )?;
722
723 let sql_counter = "
724 UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1
725 WHERE id = :parent_id";
726
727 set_ancestors_last_modified(db, existing_parent_id, now)?;
731 if update_old_parent_status {
732 db.execute_cached(sql_counter, &[(":parent_id", &existing_parent_id)])?;
733 }
734 if update_new_parent_status {
735 set_ancestors_last_modified(db, parent_id, now)?;
736 db.execute_cached(sql_counter, &[(":parent_id", &parent_id)])?;
737 }
738 Ok(())
739}
740
741fn set_ancestors_last_modified(db: &PlacesDb, parent_id: RowId, time: Timestamp) -> Result<()> {
742 let sql = "
743 WITH RECURSIVE
744 ancestors(aid) AS (
745 SELECT :parent_id
746 UNION ALL
747 SELECT parent FROM moz_bookmarks
748 JOIN ancestors ON id = aid
749 WHERE type = :type
750 )
751 UPDATE moz_bookmarks SET lastModified = :time
752 WHERE id IN ancestors
753 ";
754 db.execute_cached(
755 sql,
756 &[
757 (":parent_id", &parent_id as &dyn rusqlite::ToSql),
758 (":type", &(BookmarkType::Folder as u8)),
759 (":time", &time),
760 ],
761 )?;
762 Ok(())
763}
764
765pub fn bookmarks_get_url_for_keyword(db: &PlacesDb, keyword: &str) -> Result<Option<Url>> {
767 let bookmark_url = db.try_query_row(
768 "SELECT h.url FROM moz_keywords k
769 JOIN moz_places h ON h.id = k.place_id
770 WHERE k.keyword = :keyword",
771 &[(":keyword", &keyword)],
772 |row| row.get::<_, String>("url"),
773 true,
774 )?;
775
776 match bookmark_url {
777 Some(b) => match Url::parse(&b) {
778 Ok(u) => Ok(Some(u)),
779 Err(e) => {
780 warn!("ignoring invalid url: {:?}", e);
782 Ok(None)
783 }
784 },
785 None => Ok(None),
786 }
787}
788
789pub fn count_bookmarks_in_trees(db: &PlacesDb, item_guids: &[SyncGuid]) -> Result<u32> {
794 if item_guids.is_empty() {
795 return Ok(0);
796 }
797 let vars = repeat_sql_vars(item_guids.len());
798 let sql = format!(
799 r#"
800 WITH RECURSIVE bookmark_tree(id, parent, type)
801 AS (
802 SELECT id, parent, type FROM moz_bookmarks
803 WHERE parent IN (SELECT id from moz_bookmarks WHERE guid IN ({vars}))
804 UNION ALL
805 SELECT bm.id, bm.parent, bm.type FROM moz_bookmarks bm, bookmark_tree bt WHERE bm.parent = bt.id
806 )
807 SELECT COUNT(*) from bookmark_tree WHERE type = {};
808 "#,
809 BookmarkType::Bookmark as u8
810 );
811 let params = rusqlite::params_from_iter(item_guids);
812 Ok(db.try_query_one(&sql, params, true)?.unwrap_or_default())
813}
814
815pub fn delete_everything(db: &PlacesDb) -> Result<()> {
817 let tx = db.begin_transaction()?;
818 db.execute_batch(&format!(
819 "DELETE FROM moz_bookmarks
820 WHERE guid NOT IN ('{}', '{}', '{}', '{}', '{}');",
821 BookmarkRootGuid::Root.as_str(),
822 BookmarkRootGuid::Menu.as_str(),
823 BookmarkRootGuid::Mobile.as_str(),
824 BookmarkRootGuid::Toolbar.as_str(),
825 BookmarkRootGuid::Unfiled.as_str(),
826 ))?;
827 reset_in_tx(db, &EngineSyncAssociation::Disconnected)?;
828 tx.commit()?;
829 Ok(())
830}
831
832#[derive(Debug)]
834pub(crate) struct RawBookmark {
835 pub place_id: Option<RowId>,
836 pub row_id: RowId,
837 pub bookmark_type: BookmarkType,
838 pub parent_id: Option<RowId>,
839 pub parent_guid: Option<SyncGuid>,
840 pub position: u32,
841 pub title: Option<String>,
842 pub url: Option<Url>,
843 pub date_added: Timestamp,
844 pub date_modified: Timestamp,
845 pub guid: SyncGuid,
846 pub _sync_status: SyncStatus,
847 pub _sync_change_counter: u32,
848 pub child_count: u32,
849 pub _grandparent_id: Option<RowId>,
850}
851
852impl RawBookmark {
853 pub fn from_row(row: &Row<'_>) -> Result<Self> {
854 let place_id = row.get::<_, Option<RowId>>("fk")?;
855 Ok(Self {
856 row_id: row.get("_id")?,
857 place_id,
858 bookmark_type: BookmarkType::from_u8_with_valid_url(row.get::<_, u8>("type")?, || {
859 place_id.is_some()
860 }),
861 parent_id: row.get("_parentId")?,
862 parent_guid: row.get("parentGuid")?,
863 position: row.get("position")?,
864 title: row.get::<_, Option<String>>("title")?,
865 url: match row.get::<_, Option<String>>("url")? {
866 Some(s) => Some(Url::parse(&s)?),
867 None => None,
868 },
869 date_added: row.get("dateAdded")?,
870 date_modified: row.get("lastModified")?,
871 guid: row.get::<_, String>("guid")?.into(),
872 _sync_status: SyncStatus::from_u8(row.get::<_, u8>("_syncStatus")?),
873 _sync_change_counter: row
874 .get::<_, Option<u32>>("syncChangeCounter")?
875 .unwrap_or_default(),
876 child_count: row.get("_childCount")?,
877 _grandparent_id: row.get("_grandparentId")?,
878 })
879 }
880}
881
882const RAW_BOOKMARK_SQL: &str = "
885 SELECT
886 b.guid,
887 p.guid AS parentGuid,
888 b.position,
889 b.dateAdded,
890 b.lastModified,
891 b.type,
892 -- Note we return null for titles with an empty string.
893 NULLIF(b.title, '') AS title,
894 h.url AS url,
895 b.id AS _id,
896 b.parent AS _parentId,
897 (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
898 p.parent AS _grandParentId,
899 b.syncStatus AS _syncStatus,
900 -- the columns below don't appear in the desktop query
901 b.fk,
902 b.syncChangeCounter
903 FROM moz_bookmarks b
904 LEFT JOIN moz_bookmarks p ON p.id = b.parent
905 LEFT JOIN moz_places h ON h.id = b.fk
906";
907
908pub(crate) fn get_raw_bookmark(db: &PlacesDb, guid: &SyncGuid) -> Result<Option<RawBookmark>> {
909 db.try_query_row(
912 &format!("{} WHERE b.guid = :guid", RAW_BOOKMARK_SQL),
913 &[(":guid", guid)],
914 RawBookmark::from_row,
915 true,
916 )
917}
918
919fn get_raw_bookmarks_for_url(db: &PlacesDb, url: &Url) -> Result<Vec<RawBookmark>> {
920 db.query_rows_into_cached(
921 &format!(
922 "{} WHERE h.url_hash = hash(:url) AND h.url = :url",
923 RAW_BOOKMARK_SQL
924 ),
925 &[(":url", &url.as_str())],
926 RawBookmark::from_row,
927 )
928}
929
930fn reset_in_tx(db: &PlacesDb, assoc: &EngineSyncAssociation) -> Result<()> {
931 db.execute_batch(&format!(
934 "DELETE FROM moz_bookmarks_synced;
935
936 DELETE FROM moz_bookmarks_deleted;
937
938 UPDATE moz_bookmarks
939 SET syncChangeCounter = 1,
940 syncStatus = {}",
941 (SyncStatus::New as u8)
942 ))?;
943
944 bookmark_sync::create_synced_bookmark_roots(db)?;
947
948 put_meta(db, LAST_SYNC_META_KEY, &0)?;
951
952 match assoc {
955 EngineSyncAssociation::Disconnected => {
956 delete_meta(db, GLOBAL_SYNCID_META_KEY)?;
957 delete_meta(db, COLLECTION_SYNCID_META_KEY)?;
958 }
959 EngineSyncAssociation::Connected(ids) => {
960 put_meta(db, GLOBAL_SYNCID_META_KEY, &ids.global)?;
961 put_meta(db, COLLECTION_SYNCID_META_KEY, &ids.coll)?;
962 }
963 }
964
965 Ok(())
966}
967
968pub mod bookmark_sync {
969 use super::*;
970 use crate::bookmark_sync::SyncedBookmarkKind;
971
972 pub(crate) fn reset(db: &PlacesDb, assoc: &EngineSyncAssociation) -> Result<()> {
976 let tx = db.begin_transaction()?;
977 reset_in_tx(db, assoc)?;
978 tx.commit()?;
979 Ok(())
980 }
981
982 pub fn create_synced_bookmark_roots(db: &Connection) -> rusqlite::Result<()> {
985 fn maybe_insert(
987 db: &Connection,
988 guid: &SyncGuid,
989 parent_guid: &SyncGuid,
990 pos: u32,
991 ) -> rusqlite::Result<()> {
992 db.execute_batch(&format!(
993 "INSERT OR IGNORE INTO moz_bookmarks_synced(guid, parentGuid, kind)
994 VALUES('{guid}', '{parent_guid}', {kind});
995
996 INSERT OR IGNORE INTO moz_bookmarks_synced_structure(
997 guid, parentGuid, position)
998 VALUES('{guid}', '{parent_guid}', {pos});",
999 guid = guid.as_str(),
1000 parent_guid = parent_guid.as_str(),
1001 kind = SyncedBookmarkKind::Folder as u8,
1002 pos = pos
1003 ))?;
1004 Ok(())
1005 }
1006
1007 maybe_insert(
1010 db,
1011 &BookmarkRootGuid::Root.as_guid(),
1012 &BookmarkRootGuid::Root.as_guid(),
1013 0,
1014 )?;
1015 for (pos, user_root) in USER_CONTENT_ROOTS.iter().enumerate() {
1016 maybe_insert(
1017 db,
1018 &user_root.as_guid(),
1019 &BookmarkRootGuid::Root.as_guid(),
1020 pos as u32,
1021 )?;
1022 }
1023 Ok(())
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030 use crate::api::places_api::test::new_mem_connection;
1031 use crate::db::PlacesDb;
1032 use crate::storage::get_meta;
1033 use crate::tests::{append_invalid_bookmark, assert_json_tree, insert_json_tree};
1034 use json_tree::*;
1035 use serde_json::Value;
1036 use std::collections::HashSet;
1037
1038 fn get_pos(conn: &PlacesDb, guid: &SyncGuid) -> u32 {
1039 get_raw_bookmark(conn, guid)
1040 .expect("should work")
1041 .unwrap()
1042 .position
1043 }
1044
1045 #[test]
1046 fn test_bookmark_url_for_keyword() -> Result<()> {
1047 let conn = new_mem_connection();
1048
1049 let url = Url::parse("http://example.com").expect("valid url");
1050
1051 conn.execute_cached(
1052 "INSERT INTO moz_places (guid, url, url_hash) VALUES ('fake_guid___', :url, hash(:url))",
1053 &[(":url", &String::from(url))],
1054 )
1055 .expect("should work");
1056 let place_id = conn.last_insert_rowid();
1057
1058 conn.execute_cached(
1060 "INSERT INTO moz_keywords
1061 (keyword, place_id)
1062 VALUES
1063 ('donut', :place_id)",
1064 &[(":place_id", &place_id)],
1065 )
1066 .expect("should work");
1067
1068 assert_eq!(
1069 bookmarks_get_url_for_keyword(&conn, "donut")?,
1070 Some(Url::parse("http://example.com")?)
1071 );
1072 assert_eq!(bookmarks_get_url_for_keyword(&conn, "juice")?, None);
1073
1074 conn.execute_cached(
1076 "REPLACE INTO moz_keywords
1077 (keyword, place_id)
1078 VALUES
1079 ('ice cream', :place_id)",
1080 &[(":place_id", &place_id)],
1081 )
1082 .expect("should work");
1083
1084 assert_eq!(
1085 bookmarks_get_url_for_keyword(&conn, "ice cream")?,
1086 Some(Url::parse("http://example.com")?)
1087 );
1088 assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1089 assert_eq!(bookmarks_get_url_for_keyword(&conn, "ice")?, None);
1090
1091 Ok(())
1092 }
1093
1094 #[test]
1095 fn test_bookmark_invalid_url_for_keyword() -> Result<()> {
1096 let conn = new_mem_connection();
1097
1098 let place_id =
1099 append_invalid_bookmark(&conn, BookmarkRootGuid::Unfiled.guid(), "invalid", "badurl")
1100 .place_id;
1101
1102 conn.execute_cached(
1104 "INSERT INTO moz_keywords
1105 (keyword, place_id)
1106 VALUES
1107 ('donut', :place_id)",
1108 &[(":place_id", &place_id)],
1109 )
1110 .expect("should work");
1111
1112 assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1113
1114 Ok(())
1115 }
1116
1117 #[test]
1118 fn test_insert() -> Result<()> {
1119 let conn = new_mem_connection();
1120 let url = Url::parse("https://www.example.com")?;
1121
1122 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1123 .expect("should work");
1124
1125 let global_change_tracker = conn.global_bookmark_change_tracker();
1126 assert!(!global_change_tracker.changed(), "can't start as changed!");
1127 let bm = InsertableItem::Bookmark {
1128 b: InsertableBookmark {
1129 parent_guid: BookmarkRootGuid::Unfiled.into(),
1130 position: BookmarkPosition::Append,
1131 date_added: None,
1132 last_modified: None,
1133 guid: None,
1134 url: url.clone(),
1135 title: Some("the title".into()),
1136 },
1137 };
1138 let guid = insert_bookmark(&conn, bm)?;
1139
1140 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1142
1143 assert!(rb.place_id.is_some());
1144 assert_eq!(rb.bookmark_type, BookmarkType::Bookmark);
1145 assert_eq!(rb.parent_guid.unwrap(), BookmarkRootGuid::Unfiled);
1146 assert_eq!(rb.position, 0);
1147 assert_eq!(rb.title, Some("the title".into()));
1148 assert_eq!(rb.url, Some(url));
1149 assert_eq!(rb._sync_status, SyncStatus::New);
1150 assert_eq!(rb._sync_change_counter, 1);
1151 assert!(global_change_tracker.changed());
1152 assert_eq!(rb.child_count, 0);
1153
1154 let unfiled = get_raw_bookmark(&conn, &BookmarkRootGuid::Unfiled.as_guid())?
1155 .expect("should get unfiled");
1156 assert_eq!(unfiled._sync_change_counter, 1);
1157
1158 Ok(())
1159 }
1160
1161 #[test]
1162 fn test_insert_titles() -> Result<()> {
1163 let conn = new_mem_connection();
1164 let url = Url::parse("https://www.example.com")?;
1165
1166 let bm = InsertableItem::Bookmark {
1167 b: InsertableBookmark {
1168 parent_guid: BookmarkRootGuid::Unfiled.into(),
1169 position: BookmarkPosition::Append,
1170 date_added: None,
1171 last_modified: None,
1172 guid: None,
1173 url: url.clone(),
1174 title: Some("".into()),
1175 },
1176 };
1177 let guid = insert_bookmark(&conn, bm)?;
1178 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1179 assert_eq!(rb.title, None);
1180
1181 let bm2 = InsertableItem::Bookmark {
1182 b: InsertableBookmark {
1183 parent_guid: BookmarkRootGuid::Unfiled.into(),
1184 position: BookmarkPosition::Append,
1185 date_added: None,
1186 last_modified: None,
1187 guid: None,
1188 url,
1189 title: None,
1190 },
1191 };
1192 let guid2 = insert_bookmark(&conn, bm2)?;
1193 let rb2 = get_raw_bookmark(&conn, &guid2)?.expect("should get the bookmark");
1194 assert_eq!(rb2.title, None);
1195 Ok(())
1196 }
1197
1198 #[test]
1199 fn test_delete() -> Result<()> {
1200 let conn = new_mem_connection();
1201
1202 let guid1 = SyncGuid::random();
1203 let guid2 = SyncGuid::random();
1204 let guid2_1 = SyncGuid::random();
1205 let guid3 = SyncGuid::random();
1206
1207 let jtree = json!({
1208 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1209 "children": [
1210 {
1211 "guid": &guid1,
1212 "title": "the bookmark",
1213 "url": "https://www.example.com/"
1214 },
1215 {
1216 "guid": &guid2,
1217 "title": "A folder",
1218 "children": [
1219 {
1220 "guid": &guid2_1,
1221 "title": "bookmark in A folder",
1222 "url": "https://www.example2.com/"
1223 }
1224 ]
1225 },
1226 {
1227 "guid": &guid3,
1228 "title": "the last bookmark",
1229 "url": "https://www.example3.com/"
1230 },
1231 ]
1232 });
1233
1234 insert_json_tree(&conn, jtree);
1235
1236 conn.execute(
1237 &format!(
1238 "UPDATE moz_bookmarks SET syncChangeCounter = 1, syncStatus = {}",
1239 SyncStatus::Normal as u8
1240 ),
1241 [],
1242 )
1243 .expect("should work");
1244
1245 assert_eq!(get_pos(&conn, &guid1), 0);
1247 assert_eq!(get_pos(&conn, &guid2), 1);
1248 assert_eq!(get_pos(&conn, &guid3), 2);
1249
1250 let global_change_tracker = conn.global_bookmark_change_tracker();
1251
1252 delete_bookmark(&conn, &guid2)?;
1254 assert!(get_raw_bookmark(&conn, &guid2)?.is_none());
1256 assert!(get_raw_bookmark(&conn, &guid2_1)?.is_none());
1258 assert_eq!(get_pos(&conn, &guid1), 0);
1260 assert_eq!(get_pos(&conn, &guid3), 1);
1261 assert!(global_change_tracker.changed());
1262
1263 assert_eq!(
1264 conn.conn_ext_query_one::<i64>(
1265 "SELECT COUNT(*) FROM moz_origins WHERE host='www.example2.com';"
1266 )?,
1267 0
1268 );
1269
1270 Ok(())
1271 }
1272
1273 #[test]
1274 fn test_delete_roots() {
1275 let conn = new_mem_connection();
1276
1277 delete_bookmark(&conn, &BookmarkRootGuid::Root.into()).expect_err("can't delete root");
1278 delete_bookmark(&conn, &BookmarkRootGuid::Unfiled.into())
1279 .expect_err("can't delete any root");
1280 }
1281
1282 #[test]
1283 fn test_insert_pos_too_large() -> Result<()> {
1284 let conn = new_mem_connection();
1285 let url = Url::parse("https://www.example.com")?;
1286
1287 let bm = InsertableItem::Bookmark {
1288 b: InsertableBookmark {
1289 parent_guid: BookmarkRootGuid::Unfiled.into(),
1290 position: BookmarkPosition::Specific { pos: 100 },
1291 date_added: None,
1292 last_modified: None,
1293 guid: None,
1294 url,
1295 title: Some("the title".into()),
1296 },
1297 };
1298 let guid = insert_bookmark(&conn, bm)?;
1299
1300 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1302
1303 assert_eq!(rb.position, 0, "large value should have been ignored");
1304 Ok(())
1305 }
1306
1307 #[test]
1308 fn test_update_move_same_parent() {
1309 let conn = new_mem_connection();
1310 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1311
1312 let do_move = |guid: &str, pos: BookmarkPosition| {
1314 let global_change_tracker = conn.global_bookmark_change_tracker();
1315 update_bookmark(
1316 &conn,
1317 &guid.into(),
1318 &UpdatableBookmark {
1319 location: UpdateTreeLocation::Position { pos },
1320 ..Default::default()
1321 }
1322 .into(),
1323 )
1324 .expect("update should work");
1325 assert!(global_change_tracker.changed(), "should be tracked");
1326 };
1327
1328 let check_tree = |children: Value| {
1330 assert_json_tree(
1331 &conn,
1332 unfiled,
1333 json!({
1334 "guid": unfiled,
1335 "children": children
1336 }),
1337 );
1338 };
1339
1340 insert_json_tree(
1341 &conn,
1342 json!({
1343 "guid": unfiled,
1344 "children": [
1345 {
1346 "guid": "bookmark1___",
1347 "url": "https://www.example1.com/"
1348 },
1349 {
1350 "guid": "bookmark2___",
1351 "url": "https://www.example2.com/"
1352 },
1353 {
1354 "guid": "bookmark3___",
1355 "url": "https://www.example3.com/"
1356 },
1357
1358 ]
1359 }),
1360 );
1361
1362 do_move("bookmark2___", BookmarkPosition::Append);
1364 check_tree(json!([
1365 {"url": "https://www.example1.com/"},
1366 {"url": "https://www.example3.com/"},
1367 {"url": "https://www.example2.com/"},
1368 ]));
1369
1370 do_move("bookmark3___", BookmarkPosition::Specific { pos: 1 });
1372 check_tree(json!([
1373 {"url": "https://www.example1.com/"},
1374 {"url": "https://www.example3.com/"},
1375 {"url": "https://www.example2.com/"},
1376 ]));
1377
1378 do_move("bookmark2___", BookmarkPosition::Specific { pos: 1 });
1380 check_tree(json!([
1381 {"url": "https://www.example1.com/"},
1382 {"url": "https://www.example2.com/"},
1383 {"url": "https://www.example3.com/"},
1384 ]));
1385
1386 do_move("bookmark2___", BookmarkPosition::Specific { pos: 2 });
1388 check_tree(json!([
1389 {"url": "https://www.example1.com/"},
1390 {"url": "https://www.example3.com/"},
1391 {"url": "https://www.example2.com/"},
1392 ]));
1393
1394 do_move("bookmark1___", BookmarkPosition::Specific { pos: 10 });
1396 check_tree(json!([
1397 {"url": "https://www.example3.com/"},
1398 {"url": "https://www.example2.com/"},
1399 {"url": "https://www.example1.com/"},
1400 ]));
1401 }
1402
1403 #[test]
1404 fn test_update() -> Result<()> {
1405 let conn = new_mem_connection();
1406 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1407
1408 insert_json_tree(
1409 &conn,
1410 json!({
1411 "guid": unfiled,
1412 "children": [
1413 {
1414 "guid": "bookmark1___",
1415 "title": "the bookmark",
1416 "url": "https://www.example.com/"
1417 },
1418 {
1419 "guid": "bookmark2___",
1420 "title": "another bookmark",
1421 "url": "https://www.example2.com/"
1422 },
1423 {
1424 "guid": "folder1_____",
1425 "title": "A folder",
1426 "children": [
1427 {
1428 "guid": "bookmark3___",
1429 "title": "bookmark in A folder",
1430 "url": "https://www.example3.com/"
1431 },
1432 {
1433 "guid": "bookmark4___",
1434 "title": "next bookmark in A folder",
1435 "url": "https://www.example4.com/"
1436 },
1437 {
1438 "guid": "bookmark5___",
1439 "title": "next next bookmark in A folder",
1440 "url": "https://www.example5.com/"
1441 }
1442 ]
1443 },
1444 {
1445 "guid": "bookmark6___",
1446 "title": "yet another bookmark",
1447 "url": "https://www.example6.com/"
1448 },
1449
1450 ]
1451 }),
1452 );
1453
1454 update_bookmark(
1455 &conn,
1456 &"folder1_____".into(),
1457 &UpdatableFolder {
1458 title: Some("new name".to_string()),
1459 ..Default::default()
1460 }
1461 .into(),
1462 )?;
1463 update_bookmark(
1464 &conn,
1465 &"bookmark1___".into(),
1466 &UpdatableBookmark {
1467 url: Some(Url::parse("https://www.example3.com/")?),
1468 title: None,
1469 ..Default::default()
1470 }
1471 .into(),
1472 )?;
1473
1474 update_bookmark(
1476 &conn,
1477 &"bookmark6___".into(),
1478 &UpdatableBookmark {
1479 location: UpdateTreeLocation::Position {
1480 pos: BookmarkPosition::Specific { pos: 2 },
1481 },
1482 ..Default::default()
1483 }
1484 .into(),
1485 )?;
1486
1487 update_bookmark(
1489 &conn,
1490 &"bookmark2___".into(),
1491 &UpdatableBookmark {
1492 location: UpdateTreeLocation::Parent {
1493 guid: "folder1_____".into(),
1494 pos: BookmarkPosition::Specific { pos: 1 },
1495 },
1496 ..Default::default()
1497 }
1498 .into(),
1499 )?;
1500
1501 assert_json_tree(
1502 &conn,
1503 unfiled,
1504 json!({
1505 "guid": unfiled,
1506 "children": [
1507 {
1508 "guid": "bookmark1___",
1510 "title": null,
1511 "url": "https://www.example3.com/"
1512 },
1513 {
1517 "guid": "bookmark6___",
1518 "url": "https://www.example6.com/"
1519 },
1520 {
1521 "guid": "folder1_____",
1523 "title": "new name",
1524 "children": [
1525 {
1526 "guid": "bookmark3___",
1527 "url": "https://www.example3.com/"
1528 },
1529 {
1530 "guid": "bookmark2___",
1532 "url": "https://www.example2.com/"
1533 },
1534 {
1535 "guid": "bookmark4___",
1536 "url": "https://www.example4.com/"
1537 },
1538 {
1539 "guid": "bookmark5___",
1540 "url": "https://www.example5.com/"
1541 }
1542 ]
1543 },
1544
1545 ]
1546 }),
1547 );
1548
1549 Ok(())
1550 }
1551
1552 #[test]
1553 fn test_update_titles() -> Result<()> {
1554 let conn = new_mem_connection();
1555 let guid: SyncGuid = "bookmark1___".into();
1556
1557 insert_json_tree(
1558 &conn,
1559 json!({
1560 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1561 "children": [
1562 {
1563 "guid": "bookmark1___",
1564 "title": "the bookmark",
1565 "url": "https://www.example.com/"
1566 },
1567 ],
1568 }),
1569 );
1570
1571 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1572 .expect("should work");
1573
1574 update_bookmark(
1576 &conn,
1577 &guid,
1578 &UpdatableBookmark {
1579 title: None,
1580 ..Default::default()
1581 }
1582 .into(),
1583 )?;
1584 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1585 assert_eq!(bm.title, Some("the bookmark".to_string()));
1586 assert_eq!(bm._sync_change_counter, 0);
1587
1588 update_bookmark(
1590 &conn,
1591 &guid,
1592 &UpdatableBookmark {
1593 title: Some("the bookmark".to_string()),
1594 ..Default::default()
1595 }
1596 .into(),
1597 )?;
1598 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1599 assert_eq!(bm.title, Some("the bookmark".to_string()));
1600 assert_eq!(bm._sync_change_counter, 0);
1601
1602 update_bookmark(
1604 &conn,
1605 &guid,
1606 &UpdatableBookmark {
1607 title: Some("".to_string()),
1608 ..Default::default()
1609 }
1610 .into(),
1611 )?;
1612 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1613 assert_eq!(bm.title, None);
1614 assert_eq!(bm._sync_change_counter, 1);
1615
1616 Ok(())
1617 }
1618
1619 #[test]
1620 fn test_update_statuses() -> Result<()> {
1621 let conn = new_mem_connection();
1622 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1623
1624 let check_change_counters = |guids: Vec<&str>| {
1625 let sql = "SELECT guid FROM moz_bookmarks WHERE syncChangeCounter != 0";
1626 let mut stmt = conn.prepare(sql).expect("sql is ok");
1627 let got_guids: HashSet<String> = stmt
1628 .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1629 .expect("should work")
1630 .map(std::result::Result::unwrap)
1631 .collect();
1632
1633 assert_eq!(
1634 got_guids,
1635 guids.into_iter().map(ToString::to_string).collect()
1636 );
1637 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1639 .expect("should work");
1640 };
1641
1642 let check_last_modified = |guids: Vec<&str>| {
1643 let sql = "SELECT guid FROM moz_bookmarks
1644 WHERE lastModified >= 1000 AND guid != 'root________'";
1645
1646 let mut stmt = conn.prepare(sql).expect("sql is ok");
1647 let got_guids: HashSet<String> = stmt
1648 .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1649 .expect("should work")
1650 .map(std::result::Result::unwrap)
1651 .collect();
1652
1653 assert_eq!(
1654 got_guids,
1655 guids.into_iter().map(ToString::to_string).collect()
1656 );
1657 conn.execute("UPDATE moz_bookmarks SET lastModified = 123", [])
1659 .expect("should work");
1660 };
1661
1662 insert_json_tree(
1663 &conn,
1664 json!({
1665 "guid": unfiled,
1666 "children": [
1667 {
1668 "guid": "folder1_____",
1669 "title": "A folder",
1670 "children": [
1671 {
1672 "guid": "bookmark1___",
1673 "title": "bookmark in A folder",
1674 "url": "https://www.example2.com/"
1675 },
1676 {
1677 "guid": "bookmark2___",
1678 "title": "next bookmark in A folder",
1679 "url": "https://www.example3.com/"
1680 },
1681 ]
1682 },
1683 {
1684 "guid": "folder2_____",
1685 "title": "folder 2",
1686 },
1687 ]
1688 }),
1689 );
1690
1691 conn.execute(
1693 "UPDATE moz_bookmarks SET syncChangeCounter = 0, lastModified = 123",
1694 [],
1695 )?;
1696
1697 update_bookmark(
1699 &conn,
1700 &"bookmark1___".into(),
1701 &UpdatableBookmark {
1702 title: Some("new name".to_string()),
1703 ..Default::default()
1704 }
1705 .into(),
1706 )?;
1707 check_change_counters(vec!["bookmark1___"]);
1708 check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1710
1711 update_bookmark(
1713 &conn,
1714 &"bookmark1___".into(),
1715 &UpdatableBookmark {
1716 location: UpdateTreeLocation::Position {
1717 pos: BookmarkPosition::Append,
1718 },
1719 ..Default::default()
1720 }
1721 .into(),
1722 )?;
1723 check_change_counters(vec!["folder1_____"]);
1725 check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1727
1728 update_bookmark(
1730 &conn,
1731 &"bookmark1___".into(),
1732 &UpdatableBookmark {
1733 location: UpdateTreeLocation::Parent {
1734 guid: "folder2_____".into(),
1735 pos: BookmarkPosition::Append,
1736 },
1737 ..Default::default()
1738 }
1739 .into(),
1740 )?;
1741 check_change_counters(vec!["folder1_____", "folder2_____"]);
1743 check_last_modified(vec![
1745 "unfiled_____",
1746 "folder1_____",
1747 "folder2_____",
1748 "bookmark1___",
1749 ]);
1750
1751 Ok(())
1752 }
1753
1754 #[test]
1755 fn test_update_errors() {
1756 let conn = new_mem_connection();
1757
1758 insert_json_tree(
1759 &conn,
1760 json!({
1761 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1762 "children": [
1763 {
1764 "guid": "bookmark1___",
1765 "title": "the bookmark",
1766 "url": "https://www.example.com/"
1767 },
1768 {
1769 "guid": "folder1_____",
1770 "title": "A folder",
1771 "children": [
1772 {
1773 "guid": "bookmark2___",
1774 "title": "bookmark in A folder",
1775 "url": "https://www.example2.com/"
1776 },
1777 ]
1778 },
1779 ]
1780 }),
1781 );
1782 update_bookmark(
1784 &conn,
1785 &"bookmark9___".into(),
1786 &UpdatableBookmark {
1787 ..Default::default()
1788 }
1789 .into(),
1790 )
1791 .expect_err("should fail to update an item that doesn't exist");
1792
1793 update_bookmark(
1795 &conn,
1796 &"bookmark1___".into(),
1797 &UpdatableBookmark {
1798 location: UpdateTreeLocation::Parent {
1799 guid: "bookmark2___".into(),
1800 pos: BookmarkPosition::Specific { pos: 1 },
1801 },
1802 ..Default::default()
1803 }
1804 .into(),
1805 )
1806 .expect_err("can't move to a bookmark");
1807
1808 update_bookmark(
1810 &conn,
1811 &"bookmark1___".into(),
1812 &UpdatableBookmark {
1813 location: UpdateTreeLocation::Parent {
1814 guid: BookmarkRootGuid::Root.as_guid(),
1815 pos: BookmarkPosition::Specific { pos: 1 },
1816 },
1817 ..Default::default()
1818 }
1819 .into(),
1820 )
1821 .expect_err("can't move to the root");
1822 }
1823
1824 #[test]
1825 fn test_delete_everything() -> Result<()> {
1826 let conn = new_mem_connection();
1827
1828 insert_bookmark(
1829 &conn,
1830 InsertableFolder {
1831 parent_guid: BookmarkRootGuid::Unfiled.into(),
1832 position: BookmarkPosition::Append,
1833 date_added: None,
1834 last_modified: None,
1835 guid: Some("folderAAAAAA".into()),
1836 title: Some("A".into()),
1837 children: vec![],
1838 }
1839 .into(),
1840 )?;
1841 insert_bookmark(
1842 &conn,
1843 InsertableBookmark {
1844 parent_guid: BookmarkRootGuid::Unfiled.into(),
1845 position: BookmarkPosition::Append,
1846 date_added: None,
1847 last_modified: None,
1848 guid: Some("bookmarkBBBB".into()),
1849 url: Url::parse("http://example.com/b")?,
1850 title: Some("B".into()),
1851 }
1852 .into(),
1853 )?;
1854 insert_bookmark(
1855 &conn,
1856 InsertableBookmark {
1857 parent_guid: "folderAAAAAA".into(),
1858 position: BookmarkPosition::Append,
1859 date_added: None,
1860 last_modified: None,
1861 guid: Some("bookmarkCCCC".into()),
1862 url: Url::parse("http://example.com/c")?,
1863 title: Some("C".into()),
1864 }
1865 .into(),
1866 )?;
1867
1868 delete_everything(&conn)?;
1869
1870 let (tree, _, _) =
1871 fetch_tree(&conn, &BookmarkRootGuid::Root.into(), &FetchDepth::Deepest)?.unwrap();
1872 if let BookmarkTreeNode::Folder { f: root } = tree {
1873 assert_eq!(root.children.len(), 4);
1874 let unfiled = root
1875 .children
1876 .iter()
1877 .find(|c| c.guid() == BookmarkRootGuid::Unfiled.guid())
1878 .expect("Should return unfiled root");
1879 if let BookmarkTreeNode::Folder { f: unfiled } = unfiled {
1880 assert!(unfiled.children.is_empty());
1881 } else {
1882 panic!("The unfiled root should be a folder");
1883 }
1884 } else {
1885 panic!("`fetch_tree` should return the Places root folder");
1886 }
1887
1888 Ok(())
1889 }
1890
1891 #[test]
1892 fn test_sync_reset() -> Result<()> {
1893 let conn = new_mem_connection();
1894
1895 put_meta(&conn, GLOBAL_SYNCID_META_KEY, &"syncAAAAAAAA")?;
1897 put_meta(&conn, COLLECTION_SYNCID_META_KEY, &"syncBBBBBBBB")?;
1898 put_meta(&conn, LAST_SYNC_META_KEY, &12345)?;
1899
1900 insert_bookmark(
1901 &conn,
1902 InsertableBookmark {
1903 parent_guid: BookmarkRootGuid::Unfiled.into(),
1904 position: BookmarkPosition::Append,
1905 date_added: None,
1906 last_modified: None,
1907 guid: Some("bookmarkAAAA".into()),
1908 url: Url::parse("http://example.com/a")?,
1909 title: Some("A".into()),
1910 }
1911 .into(),
1912 )?;
1913
1914 conn.execute(
1916 &format!(
1917 "UPDATE moz_bookmarks SET
1918 syncChangeCounter = 0,
1919 syncStatus = {}",
1920 (SyncStatus::Normal as u8)
1921 ),
1922 [],
1923 )?;
1924
1925 let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1926 .expect("Should fetch A before resetting");
1927 assert_eq!(bmk._sync_change_counter, 0);
1928 assert_eq!(bmk._sync_status, SyncStatus::Normal);
1929
1930 bookmark_sync::reset(&conn, &EngineSyncAssociation::Disconnected)?;
1931
1932 let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1933 .expect("Should fetch A after resetting");
1934 assert_eq!(bmk._sync_change_counter, 1);
1935 assert_eq!(bmk._sync_status, SyncStatus::New);
1936
1937 let global = get_meta::<SyncGuid>(&conn, GLOBAL_SYNCID_META_KEY)?;
1939 assert!(global.is_none());
1940 let coll = get_meta::<SyncGuid>(&conn, COLLECTION_SYNCID_META_KEY)?;
1941 assert!(coll.is_none());
1942 let since = get_meta::<i64>(&conn, LAST_SYNC_META_KEY)?;
1943 assert_eq!(since, Some(0));
1944
1945 Ok(())
1946 }
1947
1948 #[test]
1949 fn test_count_tree() -> Result<()> {
1950 let conn = new_mem_connection();
1951 let unfiled = BookmarkRootGuid::Unfiled.as_guid();
1952
1953 insert_json_tree(
1954 &conn,
1955 json!({
1956 "guid": &unfiled,
1957 "children": [
1958 {
1959 "guid": "folder1_____",
1960 "title": "A folder",
1961 "children": [
1962 {
1963 "guid": "bookmark1___",
1964 "title": "bookmark in A folder",
1965 "url": "https://www.example2.com/"
1966 },
1967 {
1968 "guid": "separator1__",
1969 "type": BookmarkType::Separator,
1970 },
1971 {
1972 "guid": "bookmark2___",
1973 "title": "next bookmark in A folder",
1974 "url": "https://www.example3.com/"
1975 },
1976 ]
1977 },
1978 {
1979 "guid": "folder2_____",
1980 "title": "folder 2",
1981 },
1982 {
1983 "guid": "folder3_____",
1984 "title": "Another folder",
1985 "children": [
1986 {
1987 "guid": "bookmark3___",
1988 "title": "bookmark in folder 3",
1989 "url": "https://www.example2.com/"
1990 },
1991 {
1992 "guid": "separator2__",
1993 "type": BookmarkType::Separator,
1994 },
1995 {
1996 "guid": "bookmark4___",
1997 "title": "next bookmark in folder 3",
1998 "url": "https://www.example3.com/"
1999 },
2000 ]
2001 },
2002 ]
2003 }),
2004 );
2005 assert_eq!(count_bookmarks_in_trees(&conn, &[])?, 0);
2006 assert_eq!(count_bookmarks_in_trees(&conn, &[unfiled])?, 4);
2008 assert_eq!(
2010 count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder1_____")])?,
2011 2
2012 );
2013 assert_eq!(
2015 count_bookmarks_in_trees(&conn, &[SyncGuid::from("bookmark1___")])?,
2016 0
2017 );
2018 assert_eq!(
2019 count_bookmarks_in_trees(&conn, &[SyncGuid::from("no_such_guid")])?,
2020 0
2021 );
2022 assert_eq!(
2024 count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder2_____")])?,
2025 0
2026 );
2027 assert_eq!(
2029 count_bookmarks_in_trees(
2030 &conn,
2031 &[
2032 SyncGuid::from("folder1_____"),
2033 SyncGuid::from("folder3_____")
2034 ]
2035 )?,
2036 4
2037 );
2038 Ok(())
2039 }
2040}