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 pretty_assertions::assert_eq;
1036 use serde_json::Value;
1037 use std::collections::HashSet;
1038
1039 fn get_pos(conn: &PlacesDb, guid: &SyncGuid) -> u32 {
1040 get_raw_bookmark(conn, guid)
1041 .expect("should work")
1042 .unwrap()
1043 .position
1044 }
1045
1046 #[test]
1047 fn test_bookmark_url_for_keyword() -> Result<()> {
1048 let conn = new_mem_connection();
1049
1050 let url = Url::parse("http://example.com").expect("valid url");
1051
1052 conn.execute_cached(
1053 "INSERT INTO moz_places (guid, url, url_hash) VALUES ('fake_guid___', :url, hash(:url))",
1054 &[(":url", &String::from(url))],
1055 )
1056 .expect("should work");
1057 let place_id = conn.last_insert_rowid();
1058
1059 conn.execute_cached(
1061 "INSERT INTO moz_keywords
1062 (keyword, place_id)
1063 VALUES
1064 ('donut', :place_id)",
1065 &[(":place_id", &place_id)],
1066 )
1067 .expect("should work");
1068
1069 assert_eq!(
1070 bookmarks_get_url_for_keyword(&conn, "donut")?,
1071 Some(Url::parse("http://example.com")?)
1072 );
1073 assert_eq!(bookmarks_get_url_for_keyword(&conn, "juice")?, None);
1074
1075 conn.execute_cached(
1077 "REPLACE INTO moz_keywords
1078 (keyword, place_id)
1079 VALUES
1080 ('ice cream', :place_id)",
1081 &[(":place_id", &place_id)],
1082 )
1083 .expect("should work");
1084
1085 assert_eq!(
1086 bookmarks_get_url_for_keyword(&conn, "ice cream")?,
1087 Some(Url::parse("http://example.com")?)
1088 );
1089 assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1090 assert_eq!(bookmarks_get_url_for_keyword(&conn, "ice")?, None);
1091
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn test_bookmark_invalid_url_for_keyword() -> Result<()> {
1097 let conn = new_mem_connection();
1098
1099 let place_id =
1100 append_invalid_bookmark(&conn, BookmarkRootGuid::Unfiled.guid(), "invalid", "badurl")
1101 .place_id;
1102
1103 conn.execute_cached(
1105 "INSERT INTO moz_keywords
1106 (keyword, place_id)
1107 VALUES
1108 ('donut', :place_id)",
1109 &[(":place_id", &place_id)],
1110 )
1111 .expect("should work");
1112
1113 assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1114
1115 Ok(())
1116 }
1117
1118 #[test]
1119 fn test_insert() -> Result<()> {
1120 let conn = new_mem_connection();
1121 let url = Url::parse("https://www.example.com")?;
1122
1123 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1124 .expect("should work");
1125
1126 let global_change_tracker = conn.global_bookmark_change_tracker();
1127 assert!(!global_change_tracker.changed(), "can't start as changed!");
1128 let bm = InsertableItem::Bookmark {
1129 b: InsertableBookmark {
1130 parent_guid: BookmarkRootGuid::Unfiled.into(),
1131 position: BookmarkPosition::Append,
1132 date_added: None,
1133 last_modified: None,
1134 guid: None,
1135 url: url.clone(),
1136 title: Some("the title".into()),
1137 },
1138 };
1139 let guid = insert_bookmark(&conn, bm)?;
1140
1141 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1143
1144 assert!(rb.place_id.is_some());
1145 assert_eq!(rb.bookmark_type, BookmarkType::Bookmark);
1146 assert_eq!(rb.parent_guid.unwrap(), BookmarkRootGuid::Unfiled);
1147 assert_eq!(rb.position, 0);
1148 assert_eq!(rb.title, Some("the title".into()));
1149 assert_eq!(rb.url, Some(url));
1150 assert_eq!(rb._sync_status, SyncStatus::New);
1151 assert_eq!(rb._sync_change_counter, 1);
1152 assert!(global_change_tracker.changed());
1153 assert_eq!(rb.child_count, 0);
1154
1155 let unfiled = get_raw_bookmark(&conn, &BookmarkRootGuid::Unfiled.as_guid())?
1156 .expect("should get unfiled");
1157 assert_eq!(unfiled._sync_change_counter, 1);
1158
1159 Ok(())
1160 }
1161
1162 #[test]
1163 fn test_insert_titles() -> Result<()> {
1164 let conn = new_mem_connection();
1165 let url = Url::parse("https://www.example.com")?;
1166
1167 let bm = InsertableItem::Bookmark {
1168 b: InsertableBookmark {
1169 parent_guid: BookmarkRootGuid::Unfiled.into(),
1170 position: BookmarkPosition::Append,
1171 date_added: None,
1172 last_modified: None,
1173 guid: None,
1174 url: url.clone(),
1175 title: Some("".into()),
1176 },
1177 };
1178 let guid = insert_bookmark(&conn, bm)?;
1179 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1180 assert_eq!(rb.title, None);
1181
1182 let bm2 = InsertableItem::Bookmark {
1183 b: InsertableBookmark {
1184 parent_guid: BookmarkRootGuid::Unfiled.into(),
1185 position: BookmarkPosition::Append,
1186 date_added: None,
1187 last_modified: None,
1188 guid: None,
1189 url,
1190 title: None,
1191 },
1192 };
1193 let guid2 = insert_bookmark(&conn, bm2)?;
1194 let rb2 = get_raw_bookmark(&conn, &guid2)?.expect("should get the bookmark");
1195 assert_eq!(rb2.title, None);
1196 Ok(())
1197 }
1198
1199 #[test]
1200 fn test_delete() -> Result<()> {
1201 let conn = new_mem_connection();
1202
1203 let guid1 = SyncGuid::random();
1204 let guid2 = SyncGuid::random();
1205 let guid2_1 = SyncGuid::random();
1206 let guid3 = SyncGuid::random();
1207
1208 let jtree = json!({
1209 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1210 "children": [
1211 {
1212 "guid": &guid1,
1213 "title": "the bookmark",
1214 "url": "https://www.example.com/"
1215 },
1216 {
1217 "guid": &guid2,
1218 "title": "A folder",
1219 "children": [
1220 {
1221 "guid": &guid2_1,
1222 "title": "bookmark in A folder",
1223 "url": "https://www.example2.com/"
1224 }
1225 ]
1226 },
1227 {
1228 "guid": &guid3,
1229 "title": "the last bookmark",
1230 "url": "https://www.example3.com/"
1231 },
1232 ]
1233 });
1234
1235 insert_json_tree(&conn, jtree);
1236
1237 conn.execute(
1238 &format!(
1239 "UPDATE moz_bookmarks SET syncChangeCounter = 1, syncStatus = {}",
1240 SyncStatus::Normal as u8
1241 ),
1242 [],
1243 )
1244 .expect("should work");
1245
1246 assert_eq!(get_pos(&conn, &guid1), 0);
1248 assert_eq!(get_pos(&conn, &guid2), 1);
1249 assert_eq!(get_pos(&conn, &guid3), 2);
1250
1251 let global_change_tracker = conn.global_bookmark_change_tracker();
1252
1253 delete_bookmark(&conn, &guid2)?;
1255 assert!(get_raw_bookmark(&conn, &guid2)?.is_none());
1257 assert!(get_raw_bookmark(&conn, &guid2_1)?.is_none());
1259 assert_eq!(get_pos(&conn, &guid1), 0);
1261 assert_eq!(get_pos(&conn, &guid3), 1);
1262 assert!(global_change_tracker.changed());
1263
1264 assert_eq!(
1265 conn.query_one::<i64>(
1266 "SELECT COUNT(*) FROM moz_origins WHERE host='www.example2.com';"
1267 )?,
1268 0
1269 );
1270
1271 Ok(())
1272 }
1273
1274 #[test]
1275 fn test_delete_roots() {
1276 let conn = new_mem_connection();
1277
1278 delete_bookmark(&conn, &BookmarkRootGuid::Root.into()).expect_err("can't delete root");
1279 delete_bookmark(&conn, &BookmarkRootGuid::Unfiled.into())
1280 .expect_err("can't delete any root");
1281 }
1282
1283 #[test]
1284 fn test_insert_pos_too_large() -> Result<()> {
1285 let conn = new_mem_connection();
1286 let url = Url::parse("https://www.example.com")?;
1287
1288 let bm = InsertableItem::Bookmark {
1289 b: InsertableBookmark {
1290 parent_guid: BookmarkRootGuid::Unfiled.into(),
1291 position: BookmarkPosition::Specific { pos: 100 },
1292 date_added: None,
1293 last_modified: None,
1294 guid: None,
1295 url,
1296 title: Some("the title".into()),
1297 },
1298 };
1299 let guid = insert_bookmark(&conn, bm)?;
1300
1301 let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1303
1304 assert_eq!(rb.position, 0, "large value should have been ignored");
1305 Ok(())
1306 }
1307
1308 #[test]
1309 fn test_update_move_same_parent() {
1310 let conn = new_mem_connection();
1311 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1312
1313 let do_move = |guid: &str, pos: BookmarkPosition| {
1315 let global_change_tracker = conn.global_bookmark_change_tracker();
1316 update_bookmark(
1317 &conn,
1318 &guid.into(),
1319 &UpdatableBookmark {
1320 location: UpdateTreeLocation::Position { pos },
1321 ..Default::default()
1322 }
1323 .into(),
1324 )
1325 .expect("update should work");
1326 assert!(global_change_tracker.changed(), "should be tracked");
1327 };
1328
1329 let check_tree = |children: Value| {
1331 assert_json_tree(
1332 &conn,
1333 unfiled,
1334 json!({
1335 "guid": unfiled,
1336 "children": children
1337 }),
1338 );
1339 };
1340
1341 insert_json_tree(
1342 &conn,
1343 json!({
1344 "guid": unfiled,
1345 "children": [
1346 {
1347 "guid": "bookmark1___",
1348 "url": "https://www.example1.com/"
1349 },
1350 {
1351 "guid": "bookmark2___",
1352 "url": "https://www.example2.com/"
1353 },
1354 {
1355 "guid": "bookmark3___",
1356 "url": "https://www.example3.com/"
1357 },
1358
1359 ]
1360 }),
1361 );
1362
1363 do_move("bookmark2___", BookmarkPosition::Append);
1365 check_tree(json!([
1366 {"url": "https://www.example1.com/"},
1367 {"url": "https://www.example3.com/"},
1368 {"url": "https://www.example2.com/"},
1369 ]));
1370
1371 do_move("bookmark3___", BookmarkPosition::Specific { pos: 1 });
1373 check_tree(json!([
1374 {"url": "https://www.example1.com/"},
1375 {"url": "https://www.example3.com/"},
1376 {"url": "https://www.example2.com/"},
1377 ]));
1378
1379 do_move("bookmark2___", BookmarkPosition::Specific { pos: 1 });
1381 check_tree(json!([
1382 {"url": "https://www.example1.com/"},
1383 {"url": "https://www.example2.com/"},
1384 {"url": "https://www.example3.com/"},
1385 ]));
1386
1387 do_move("bookmark2___", BookmarkPosition::Specific { pos: 2 });
1389 check_tree(json!([
1390 {"url": "https://www.example1.com/"},
1391 {"url": "https://www.example3.com/"},
1392 {"url": "https://www.example2.com/"},
1393 ]));
1394
1395 do_move("bookmark1___", BookmarkPosition::Specific { pos: 10 });
1397 check_tree(json!([
1398 {"url": "https://www.example3.com/"},
1399 {"url": "https://www.example2.com/"},
1400 {"url": "https://www.example1.com/"},
1401 ]));
1402 }
1403
1404 #[test]
1405 fn test_update() -> Result<()> {
1406 let conn = new_mem_connection();
1407 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1408
1409 insert_json_tree(
1410 &conn,
1411 json!({
1412 "guid": unfiled,
1413 "children": [
1414 {
1415 "guid": "bookmark1___",
1416 "title": "the bookmark",
1417 "url": "https://www.example.com/"
1418 },
1419 {
1420 "guid": "bookmark2___",
1421 "title": "another bookmark",
1422 "url": "https://www.example2.com/"
1423 },
1424 {
1425 "guid": "folder1_____",
1426 "title": "A folder",
1427 "children": [
1428 {
1429 "guid": "bookmark3___",
1430 "title": "bookmark in A folder",
1431 "url": "https://www.example3.com/"
1432 },
1433 {
1434 "guid": "bookmark4___",
1435 "title": "next bookmark in A folder",
1436 "url": "https://www.example4.com/"
1437 },
1438 {
1439 "guid": "bookmark5___",
1440 "title": "next next bookmark in A folder",
1441 "url": "https://www.example5.com/"
1442 }
1443 ]
1444 },
1445 {
1446 "guid": "bookmark6___",
1447 "title": "yet another bookmark",
1448 "url": "https://www.example6.com/"
1449 },
1450
1451 ]
1452 }),
1453 );
1454
1455 update_bookmark(
1456 &conn,
1457 &"folder1_____".into(),
1458 &UpdatableFolder {
1459 title: Some("new name".to_string()),
1460 ..Default::default()
1461 }
1462 .into(),
1463 )?;
1464 update_bookmark(
1465 &conn,
1466 &"bookmark1___".into(),
1467 &UpdatableBookmark {
1468 url: Some(Url::parse("https://www.example3.com/")?),
1469 title: None,
1470 ..Default::default()
1471 }
1472 .into(),
1473 )?;
1474
1475 update_bookmark(
1477 &conn,
1478 &"bookmark6___".into(),
1479 &UpdatableBookmark {
1480 location: UpdateTreeLocation::Position {
1481 pos: BookmarkPosition::Specific { pos: 2 },
1482 },
1483 ..Default::default()
1484 }
1485 .into(),
1486 )?;
1487
1488 update_bookmark(
1490 &conn,
1491 &"bookmark2___".into(),
1492 &UpdatableBookmark {
1493 location: UpdateTreeLocation::Parent {
1494 guid: "folder1_____".into(),
1495 pos: BookmarkPosition::Specific { pos: 1 },
1496 },
1497 ..Default::default()
1498 }
1499 .into(),
1500 )?;
1501
1502 assert_json_tree(
1503 &conn,
1504 unfiled,
1505 json!({
1506 "guid": unfiled,
1507 "children": [
1508 {
1509 "guid": "bookmark1___",
1511 "title": null,
1512 "url": "https://www.example3.com/"
1513 },
1514 {
1518 "guid": "bookmark6___",
1519 "url": "https://www.example6.com/"
1520 },
1521 {
1522 "guid": "folder1_____",
1524 "title": "new name",
1525 "children": [
1526 {
1527 "guid": "bookmark3___",
1528 "url": "https://www.example3.com/"
1529 },
1530 {
1531 "guid": "bookmark2___",
1533 "url": "https://www.example2.com/"
1534 },
1535 {
1536 "guid": "bookmark4___",
1537 "url": "https://www.example4.com/"
1538 },
1539 {
1540 "guid": "bookmark5___",
1541 "url": "https://www.example5.com/"
1542 }
1543 ]
1544 },
1545
1546 ]
1547 }),
1548 );
1549
1550 Ok(())
1551 }
1552
1553 #[test]
1554 fn test_update_titles() -> Result<()> {
1555 let conn = new_mem_connection();
1556 let guid: SyncGuid = "bookmark1___".into();
1557
1558 insert_json_tree(
1559 &conn,
1560 json!({
1561 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1562 "children": [
1563 {
1564 "guid": "bookmark1___",
1565 "title": "the bookmark",
1566 "url": "https://www.example.com/"
1567 },
1568 ],
1569 }),
1570 );
1571
1572 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1573 .expect("should work");
1574
1575 update_bookmark(
1577 &conn,
1578 &guid,
1579 &UpdatableBookmark {
1580 title: None,
1581 ..Default::default()
1582 }
1583 .into(),
1584 )?;
1585 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1586 assert_eq!(bm.title, Some("the bookmark".to_string()));
1587 assert_eq!(bm._sync_change_counter, 0);
1588
1589 update_bookmark(
1591 &conn,
1592 &guid,
1593 &UpdatableBookmark {
1594 title: Some("the bookmark".to_string()),
1595 ..Default::default()
1596 }
1597 .into(),
1598 )?;
1599 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1600 assert_eq!(bm.title, Some("the bookmark".to_string()));
1601 assert_eq!(bm._sync_change_counter, 0);
1602
1603 update_bookmark(
1605 &conn,
1606 &guid,
1607 &UpdatableBookmark {
1608 title: Some("".to_string()),
1609 ..Default::default()
1610 }
1611 .into(),
1612 )?;
1613 let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1614 assert_eq!(bm.title, None);
1615 assert_eq!(bm._sync_change_counter, 1);
1616
1617 Ok(())
1618 }
1619
1620 #[test]
1621 fn test_update_statuses() -> Result<()> {
1622 let conn = new_mem_connection();
1623 let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1624
1625 let check_change_counters = |guids: Vec<&str>| {
1626 let sql = "SELECT guid FROM moz_bookmarks WHERE syncChangeCounter != 0";
1627 let mut stmt = conn.prepare(sql).expect("sql is ok");
1628 let got_guids: HashSet<String> = stmt
1629 .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1630 .expect("should work")
1631 .map(std::result::Result::unwrap)
1632 .collect();
1633
1634 assert_eq!(
1635 got_guids,
1636 guids.into_iter().map(ToString::to_string).collect()
1637 );
1638 conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1640 .expect("should work");
1641 };
1642
1643 let check_last_modified = |guids: Vec<&str>| {
1644 let sql = "SELECT guid FROM moz_bookmarks
1645 WHERE lastModified >= 1000 AND guid != 'root________'";
1646
1647 let mut stmt = conn.prepare(sql).expect("sql is ok");
1648 let got_guids: HashSet<String> = stmt
1649 .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1650 .expect("should work")
1651 .map(std::result::Result::unwrap)
1652 .collect();
1653
1654 assert_eq!(
1655 got_guids,
1656 guids.into_iter().map(ToString::to_string).collect()
1657 );
1658 conn.execute("UPDATE moz_bookmarks SET lastModified = 123", [])
1660 .expect("should work");
1661 };
1662
1663 insert_json_tree(
1664 &conn,
1665 json!({
1666 "guid": unfiled,
1667 "children": [
1668 {
1669 "guid": "folder1_____",
1670 "title": "A folder",
1671 "children": [
1672 {
1673 "guid": "bookmark1___",
1674 "title": "bookmark in A folder",
1675 "url": "https://www.example2.com/"
1676 },
1677 {
1678 "guid": "bookmark2___",
1679 "title": "next bookmark in A folder",
1680 "url": "https://www.example3.com/"
1681 },
1682 ]
1683 },
1684 {
1685 "guid": "folder2_____",
1686 "title": "folder 2",
1687 },
1688 ]
1689 }),
1690 );
1691
1692 conn.execute(
1694 "UPDATE moz_bookmarks SET syncChangeCounter = 0, lastModified = 123",
1695 [],
1696 )?;
1697
1698 update_bookmark(
1700 &conn,
1701 &"bookmark1___".into(),
1702 &UpdatableBookmark {
1703 title: Some("new name".to_string()),
1704 ..Default::default()
1705 }
1706 .into(),
1707 )?;
1708 check_change_counters(vec!["bookmark1___"]);
1709 check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1711
1712 update_bookmark(
1714 &conn,
1715 &"bookmark1___".into(),
1716 &UpdatableBookmark {
1717 location: UpdateTreeLocation::Position {
1718 pos: BookmarkPosition::Append,
1719 },
1720 ..Default::default()
1721 }
1722 .into(),
1723 )?;
1724 check_change_counters(vec!["folder1_____"]);
1726 check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1728
1729 update_bookmark(
1731 &conn,
1732 &"bookmark1___".into(),
1733 &UpdatableBookmark {
1734 location: UpdateTreeLocation::Parent {
1735 guid: "folder2_____".into(),
1736 pos: BookmarkPosition::Append,
1737 },
1738 ..Default::default()
1739 }
1740 .into(),
1741 )?;
1742 check_change_counters(vec!["folder1_____", "folder2_____"]);
1744 check_last_modified(vec![
1746 "unfiled_____",
1747 "folder1_____",
1748 "folder2_____",
1749 "bookmark1___",
1750 ]);
1751
1752 Ok(())
1753 }
1754
1755 #[test]
1756 fn test_update_errors() {
1757 let conn = new_mem_connection();
1758
1759 insert_json_tree(
1760 &conn,
1761 json!({
1762 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1763 "children": [
1764 {
1765 "guid": "bookmark1___",
1766 "title": "the bookmark",
1767 "url": "https://www.example.com/"
1768 },
1769 {
1770 "guid": "folder1_____",
1771 "title": "A folder",
1772 "children": [
1773 {
1774 "guid": "bookmark2___",
1775 "title": "bookmark in A folder",
1776 "url": "https://www.example2.com/"
1777 },
1778 ]
1779 },
1780 ]
1781 }),
1782 );
1783 update_bookmark(
1785 &conn,
1786 &"bookmark9___".into(),
1787 &UpdatableBookmark {
1788 ..Default::default()
1789 }
1790 .into(),
1791 )
1792 .expect_err("should fail to update an item that doesn't exist");
1793
1794 update_bookmark(
1796 &conn,
1797 &"bookmark1___".into(),
1798 &UpdatableBookmark {
1799 location: UpdateTreeLocation::Parent {
1800 guid: "bookmark2___".into(),
1801 pos: BookmarkPosition::Specific { pos: 1 },
1802 },
1803 ..Default::default()
1804 }
1805 .into(),
1806 )
1807 .expect_err("can't move to a bookmark");
1808
1809 update_bookmark(
1811 &conn,
1812 &"bookmark1___".into(),
1813 &UpdatableBookmark {
1814 location: UpdateTreeLocation::Parent {
1815 guid: BookmarkRootGuid::Root.as_guid(),
1816 pos: BookmarkPosition::Specific { pos: 1 },
1817 },
1818 ..Default::default()
1819 }
1820 .into(),
1821 )
1822 .expect_err("can't move to the root");
1823 }
1824
1825 #[test]
1826 fn test_delete_everything() -> Result<()> {
1827 let conn = new_mem_connection();
1828
1829 insert_bookmark(
1830 &conn,
1831 InsertableFolder {
1832 parent_guid: BookmarkRootGuid::Unfiled.into(),
1833 position: BookmarkPosition::Append,
1834 date_added: None,
1835 last_modified: None,
1836 guid: Some("folderAAAAAA".into()),
1837 title: Some("A".into()),
1838 children: vec![],
1839 }
1840 .into(),
1841 )?;
1842 insert_bookmark(
1843 &conn,
1844 InsertableBookmark {
1845 parent_guid: BookmarkRootGuid::Unfiled.into(),
1846 position: BookmarkPosition::Append,
1847 date_added: None,
1848 last_modified: None,
1849 guid: Some("bookmarkBBBB".into()),
1850 url: Url::parse("http://example.com/b")?,
1851 title: Some("B".into()),
1852 }
1853 .into(),
1854 )?;
1855 insert_bookmark(
1856 &conn,
1857 InsertableBookmark {
1858 parent_guid: "folderAAAAAA".into(),
1859 position: BookmarkPosition::Append,
1860 date_added: None,
1861 last_modified: None,
1862 guid: Some("bookmarkCCCC".into()),
1863 url: Url::parse("http://example.com/c")?,
1864 title: Some("C".into()),
1865 }
1866 .into(),
1867 )?;
1868
1869 delete_everything(&conn)?;
1870
1871 let (tree, _, _) =
1872 fetch_tree(&conn, &BookmarkRootGuid::Root.into(), &FetchDepth::Deepest)?.unwrap();
1873 if let BookmarkTreeNode::Folder { f: root } = tree {
1874 assert_eq!(root.children.len(), 4);
1875 let unfiled = root
1876 .children
1877 .iter()
1878 .find(|c| c.guid() == BookmarkRootGuid::Unfiled.guid())
1879 .expect("Should return unfiled root");
1880 if let BookmarkTreeNode::Folder { f: unfiled } = unfiled {
1881 assert!(unfiled.children.is_empty());
1882 } else {
1883 panic!("The unfiled root should be a folder");
1884 }
1885 } else {
1886 panic!("`fetch_tree` should return the Places root folder");
1887 }
1888
1889 Ok(())
1890 }
1891
1892 #[test]
1893 fn test_sync_reset() -> Result<()> {
1894 let conn = new_mem_connection();
1895
1896 put_meta(&conn, GLOBAL_SYNCID_META_KEY, &"syncAAAAAAAA")?;
1898 put_meta(&conn, COLLECTION_SYNCID_META_KEY, &"syncBBBBBBBB")?;
1899 put_meta(&conn, LAST_SYNC_META_KEY, &12345)?;
1900
1901 insert_bookmark(
1902 &conn,
1903 InsertableBookmark {
1904 parent_guid: BookmarkRootGuid::Unfiled.into(),
1905 position: BookmarkPosition::Append,
1906 date_added: None,
1907 last_modified: None,
1908 guid: Some("bookmarkAAAA".into()),
1909 url: Url::parse("http://example.com/a")?,
1910 title: Some("A".into()),
1911 }
1912 .into(),
1913 )?;
1914
1915 conn.execute(
1917 &format!(
1918 "UPDATE moz_bookmarks SET
1919 syncChangeCounter = 0,
1920 syncStatus = {}",
1921 (SyncStatus::Normal as u8)
1922 ),
1923 [],
1924 )?;
1925
1926 let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1927 .expect("Should fetch A before resetting");
1928 assert_eq!(bmk._sync_change_counter, 0);
1929 assert_eq!(bmk._sync_status, SyncStatus::Normal);
1930
1931 bookmark_sync::reset(&conn, &EngineSyncAssociation::Disconnected)?;
1932
1933 let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1934 .expect("Should fetch A after resetting");
1935 assert_eq!(bmk._sync_change_counter, 1);
1936 assert_eq!(bmk._sync_status, SyncStatus::New);
1937
1938 let global = get_meta::<SyncGuid>(&conn, GLOBAL_SYNCID_META_KEY)?;
1940 assert!(global.is_none());
1941 let coll = get_meta::<SyncGuid>(&conn, COLLECTION_SYNCID_META_KEY)?;
1942 assert!(coll.is_none());
1943 let since = get_meta::<i64>(&conn, LAST_SYNC_META_KEY)?;
1944 assert_eq!(since, Some(0));
1945
1946 Ok(())
1947 }
1948
1949 #[test]
1950 fn test_count_tree() -> Result<()> {
1951 let conn = new_mem_connection();
1952 let unfiled = BookmarkRootGuid::Unfiled.as_guid();
1953
1954 insert_json_tree(
1955 &conn,
1956 json!({
1957 "guid": &unfiled,
1958 "children": [
1959 {
1960 "guid": "folder1_____",
1961 "title": "A folder",
1962 "children": [
1963 {
1964 "guid": "bookmark1___",
1965 "title": "bookmark in A folder",
1966 "url": "https://www.example2.com/"
1967 },
1968 {
1969 "guid": "separator1__",
1970 "type": BookmarkType::Separator,
1971 },
1972 {
1973 "guid": "bookmark2___",
1974 "title": "next bookmark in A folder",
1975 "url": "https://www.example3.com/"
1976 },
1977 ]
1978 },
1979 {
1980 "guid": "folder2_____",
1981 "title": "folder 2",
1982 },
1983 {
1984 "guid": "folder3_____",
1985 "title": "Another folder",
1986 "children": [
1987 {
1988 "guid": "bookmark3___",
1989 "title": "bookmark in folder 3",
1990 "url": "https://www.example2.com/"
1991 },
1992 {
1993 "guid": "separator2__",
1994 "type": BookmarkType::Separator,
1995 },
1996 {
1997 "guid": "bookmark4___",
1998 "title": "next bookmark in folder 3",
1999 "url": "https://www.example3.com/"
2000 },
2001 ]
2002 },
2003 ]
2004 }),
2005 );
2006 assert_eq!(count_bookmarks_in_trees(&conn, &[])?, 0);
2007 assert_eq!(count_bookmarks_in_trees(&conn, &[unfiled])?, 4);
2009 assert_eq!(
2011 count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder1_____")])?,
2012 2
2013 );
2014 assert_eq!(
2016 count_bookmarks_in_trees(&conn, &[SyncGuid::from("bookmark1___")])?,
2017 0
2018 );
2019 assert_eq!(
2020 count_bookmarks_in_trees(&conn, &[SyncGuid::from("no_such_guid")])?,
2021 0
2022 );
2023 assert_eq!(
2025 count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder2_____")])?,
2026 0
2027 );
2028 assert_eq!(
2030 count_bookmarks_in_trees(
2031 &conn,
2032 &[
2033 SyncGuid::from("folder1_____"),
2034 SyncGuid::from("folder3_____")
2035 ]
2036 )?,
2037 4
2038 );
2039 Ok(())
2040 }
2041}