places/storage/
bookmarks.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use 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
79/// Helpers to deal with managing the position correctly.
80///
81/// Updates the position of existing items so that the insertion of a child in
82/// the position specified leaves all siblings with the correct position.
83/// Returns the index the item should be inserted at.
84fn 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            // must reorder existing children.
93            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
108/// Updates the position of existing items so that the deletion of a child
109/// from the position specified leaves all siblings with the correct position.
110fn 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
123/// Updates the position of existing items when an item is being moved in the
124/// same folder.
125/// Returns what the position should be updated to.
126fn 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    // Note the additional -1's below are to account for the item already being
134    // in the folder.
135    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/// Structures which can be used to insert a bookmark, folder or separator.
159#[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// The type used to insert the actual item.
209#[derive(Debug, Clone)]
210pub enum InsertableItem {
211    Bookmark { b: InsertableBookmark },
212    Separator { s: InsertableSeparator },
213    Folder { f: InsertableFolder },
214}
215
216// We allow all "common" fields from the sub-types to be getters on the
217// InsertableItem type.
218macro_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    // We allow a setter for parent_guid and timestamps to help when inserting a tree.
245    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    // find the row ID of the parent.
289    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    // Do the "position" dance.
299    let position = resolve_pos_for_insert(db, *bm.position(), &parent)?;
300
301    // Note that we could probably do this 'fk' work as a sub-query (although
302    // markh isn't clear how we could perform the insert) - it probably doesn't
303    // matter in practice though...
304    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    // last_modified can't be before date_added
326    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            // now recurse for children
383            for mut child in f.children.into_iter() {
384                // As a special case for trees, each child in a folder can specify
385                // Guid::Empty as the parent.
386                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 children have defaults for last_modified and date_added we use the parent
395                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    // Bump the parent's change counter.
407    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
415/// Delete the specified bookmark. Returns true if a bookmark with the guid
416/// existed and was deleted, false otherwise.
417pub 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    // Can't delete a root.
429    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    // There's an argument to be made here that we should still honor the
440    // deletion in the case of this corruption, since it would be fixed by
441    // performing the deletion, and the user wants it gone...
442    let record_parent_id = record
443        .parent_id
444        .ok_or_else(|| Corruption::NonRootWithoutParent(guid.to_string()))?;
445    // must reorder existing children.
446    update_pos_for_deletion(db, record.position, record_parent_id)?;
447    // and delete - children are recursively deleted.
448    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/// Support for modifying bookmarks, including changing the location in
457/// the tree.
458
459// Used to specify how the location of the item in the tree should be updated.
460#[derive(Debug, Default, Clone)]
461pub enum UpdateTreeLocation {
462    #[default]
463    None, // no change
464    Position {
465        pos: BookmarkPosition,
466    }, // new position in the same folder.
467    Parent {
468        guid: SyncGuid,
469        pos: BookmarkPosition,
470    }, // new parent
471}
472
473/// Structures which can be used to update a bookmark, folder or separator.
474/// Almost all fields are Option<>-like, with None meaning "do not change".
475/// Many fields which can't be changed by our public API are omitted (eg,
476/// guid, date_added, last_modified, etc)
477#[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// The type used to update the actual item.
514#[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/// We don't require bookmark type for updates on the other side of the FFI,
540/// since the type is immutable, and iOS wants to be able to move bookmarks by
541/// GUID. We also don't/can't enforce as much in the Kotlin/Swift type system
542/// as we can/do in Rust.
543///
544/// This is a type that represents the data we get from the FFI, which we then
545/// turn into a `UpdatableItem` that we can actually use (we do this by
546/// reading the type out of the DB, but we can do that transactionally, so it's
547/// not a problem).
548///
549/// It's basically an intermediate between the protobuf message format and
550/// `UpdatableItem`, used to avoid needing to pass in the `type` to update, and
551/// to give us a place to check things that we can't enforce in Swift/Kotlin's
552/// type system, but that we do in Rust's.
553#[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    // Note: `tx` automatically rolls back on drop if we don't commit
580    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 guid is root
591    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    // to make our life easier we update every field, using existing when
614    // no value is specified.
615    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            // Updating a non-bookmark item, so the existing item must not
668            // have a place_id
669            assert_eq!(raw.place_id, None);
670            None
671        }
672    };
673    // While we could let the SQL take care of being clever about the update
674    // via, say `title = NULLIF(IFNULL(:title, title), '')`, this code needs
675    // to know if it changed so the sync counter can be managed correctly.
676    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        // We don't differentiate between null and the empty string for title,
685        // just like desktop doesn't post bug 1360872, hence an empty string
686        // means "set to null".
687        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    // The lastModified of the existing parent ancestors (which may still be
728    // the current parent) is always updated, even if the change counter for it
729    // isn't.
730    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
765/// Get the URL of the bookmark matching a keyword
766pub 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                // We don't have the guid to log and the keyword is PII...
781                warn!("ignoring invalid url: {:?}", e);
782                Ok(None)
783            }
784        },
785        None => Ok(None),
786    }
787}
788
789// Counts the number of bookmark items in the bookmark trees under the specified GUIDs.
790// Does not count folder items, separators. A set of empty folders will return zero, as will
791// a set of non-existing GUIDs or guids of a non-folder item.
792// The result is implementation dependant if the trees overlap.
793pub 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
815/// Erases all bookmarks and resets all Sync metadata.
816pub 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/// A "raw" bookmark - a representation of the row and some summary fields.
833#[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
882/// sql is based on fetchBookmark() in Desktop's Bookmarks.jsm, with 'fk' added
883/// and title's NULLIF handling.
884const 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    // sql is based on fetchBookmark() in Desktop's Bookmarks.jsm, with 'fk' added
910    // and title's NULLIF handling.
911    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    // Remove all synced bookmarks and pending tombstones, and mark all
932    // local bookmarks as new.
933    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    // Recreate the set of synced roots, since we just removed all synced
945    // bookmarks.
946    bookmark_sync::create_synced_bookmark_roots(db)?;
947
948    // Reset the last sync time, so that the next sync fetches fresh records
949    // from the server.
950    put_meta(db, LAST_SYNC_META_KEY, &0)?;
951
952    // Clear the sync ID if we're signing out, or set it to whatever the
953    // server gave us if we're signing in.
954    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    /// Removes all sync metadata, including synced bookmarks, pending tombstones,
973    /// change counters, sync statuses, the last sync time, and sync ID. This
974    /// should be called when the user signs out of Sync.
975    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    /// Sets up the syncable roots. All items in `moz_bookmarks_synced` descend
983    /// from these roots.
984    pub fn create_synced_bookmark_roots(db: &Connection) -> rusqlite::Result<()> {
985        // NOTE: This is called in a transaction.
986        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        // The Places root is its own parent, to ensure it's always in
1008        // `moz_bookmarks_synced_structure`.
1009        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        // create a bookmark with keyword 'donut' pointing at it.
1060        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        // now change the keyword to 'ice cream'
1076        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        // create a bookmark with keyword 'donut' pointing at it.
1104        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        // re-fetch it.
1142        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        // Make sure the positions are correct now.
1247        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 the middle folder.
1254        delete_bookmark(&conn, &guid2)?;
1255        // Should no longer exist.
1256        assert!(get_raw_bookmark(&conn, &guid2)?.is_none());
1257        // Neither should the child.
1258        assert!(get_raw_bookmark(&conn, &guid2_1)?.is_none());
1259        // Positions of the remaining should be correct.
1260        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        // re-fetch it.
1302        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        // A helper to make the moves below more concise.
1314        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        // A helper to make the checks below more concise.
1330        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        // Move a bookmark to the end.
1364        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        // Move a bookmark to its existing position
1372        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        // Move a bookmark back 1 position.
1380        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        // Move a bookmark forward 1 position.
1388        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        // Move a bookmark beyond the end.
1396        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        // A move in the same folder.
1476        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        // A move across folders.
1489        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                        // We updated the url and title of this.
1510                        "guid": "bookmark1___",
1511                        "title": null,
1512                        "url": "https://www.example3.com/"
1513                    },
1514                        // We moved bookmark6 to position=2 (ie, 3rd) of the same
1515                        // parent, but then moved the existing 2nd item to the
1516                        // folder, so this ends up second.
1517                    {
1518                        "guid": "bookmark6___",
1519                        "url": "https://www.example6.com/"
1520                    },
1521                    {
1522                        // We changed the name of the folder.
1523                        "guid": "folder1_____",
1524                        "title": "new name",
1525                        "children": [
1526                            {
1527                                "guid": "bookmark3___",
1528                                "url": "https://www.example3.com/"
1529                            },
1530                            {
1531                                // This was moved from the parent to position 1
1532                                "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 of None means no change.
1576        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 to the same value is still not a change.
1590        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 to an empty string sets it to null
1604        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            // reset them all back
1639            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            // reset them all back
1659            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        // reset all statuses and timestamps.
1693        conn.execute(
1694            "UPDATE moz_bookmarks SET syncChangeCounter = 0, lastModified = 123",
1695            [],
1696        )?;
1697
1698        // update a title - should get a change counter.
1699        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        // last modified should be all the way up the tree.
1710        check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1711
1712        // update the position in the same folder.
1713        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        // parent should be the only thing with a change counter.
1725        check_change_counters(vec!["folder1_____"]);
1726        // last modified should be all the way up the tree.
1727        check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1728
1729        // update the position to a different folder.
1730        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        // Both parents should have a change counter.
1743        check_change_counters(vec!["folder1_____", "folder2_____"]);
1744        // last modified should be all the way up the tree and include both parents.
1745        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 an item that doesn't exist.
1784        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        // A move across to a non-folder
1795        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        // A move to the root
1810        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        // Add Sync metadata keys, to ensure they're reset.
1897        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        // Mark all items as synced.
1916        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        // Ensure we reset Sync metadata, too.
1939        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        // A folder with sub-folders
2008        assert_eq!(count_bookmarks_in_trees(&conn, &[unfiled])?, 4);
2009        // A folder with items but no folders.
2010        assert_eq!(
2011            count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder1_____")])?,
2012            2
2013        );
2014        // Asking for a bookmark (ie, not a folder) or an invalid guid gives zero.
2015        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        // empty folder also zero.
2024        assert_eq!(
2025            count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder2_____")])?,
2026            0
2027        );
2028        // multiple folders
2029        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}