1use crate::error::{warn, Result};
15use crate::types::BookmarkType;
16use crate::db::PlacesDb;
18use rusqlite::Row;
19use sql_support::ConnExt;
20use std::collections::HashMap;
21use sync_guid::Guid as SyncGuid;
22use types::Timestamp;
23use url::Url;
24
25use super::{
26 BookmarkPosition, InsertableBookmark, InsertableFolder, InsertableItem, InsertableSeparator,
27 RowId,
28};
29
30use serde::{
31 de::{Deserialize, Deserializer},
32 ser::{Serialize, SerializeStruct, Serializer},
33};
34use serde_derive::*;
35
36#[cfg(test)]
44fn cmp_options<T: PartialEq>(s: &Option<T>, o: &Option<T>) -> bool {
45 match (s, o) {
46 (None, None) => true,
47 (None, Some(_)) => true,
48 (Some(_), None) => true,
49 (s, o) => s == o,
50 }
51}
52
53#[derive(Debug)]
54pub struct BookmarkNode {
55 pub guid: Option<SyncGuid>,
56 pub date_added: Option<Timestamp>,
57 pub last_modified: Option<Timestamp>,
58 pub title: Option<String>,
59 pub url: Url,
60}
61
62impl From<BookmarkNode> for BookmarkTreeNode {
63 fn from(b: BookmarkNode) -> Self {
64 BookmarkTreeNode::Bookmark { b }
65 }
66}
67
68#[cfg(test)]
69impl PartialEq for BookmarkNode {
70 fn eq(&self, other: &BookmarkNode) -> bool {
71 cmp_options(&self.guid, &other.guid)
72 && cmp_options(&self.date_added, &other.date_added)
73 && cmp_options(&self.last_modified, &other.last_modified)
74 && cmp_options(&self.title, &other.title)
75 && self.url == other.url
76 }
77}
78
79#[derive(Debug, Default)]
80pub struct SeparatorNode {
81 pub guid: Option<SyncGuid>,
82 pub date_added: Option<Timestamp>,
83 pub last_modified: Option<Timestamp>,
84}
85
86impl From<SeparatorNode> for BookmarkTreeNode {
87 fn from(s: SeparatorNode) -> Self {
88 BookmarkTreeNode::Separator { s }
89 }
90}
91
92#[cfg(test)]
93impl PartialEq for SeparatorNode {
94 fn eq(&self, other: &SeparatorNode) -> bool {
95 cmp_options(&self.guid, &other.guid)
96 && cmp_options(&self.date_added, &other.date_added)
97 && cmp_options(&self.last_modified, &other.last_modified)
98 }
99}
100
101#[derive(Debug, Default)]
102pub struct FolderNode {
103 pub guid: Option<SyncGuid>,
104 pub date_added: Option<Timestamp>,
105 pub last_modified: Option<Timestamp>,
106 pub title: Option<String>,
107 pub children: Vec<BookmarkTreeNode>,
108}
109
110impl From<FolderNode> for BookmarkTreeNode {
111 fn from(f: FolderNode) -> Self {
112 BookmarkTreeNode::Folder { f }
113 }
114}
115
116#[cfg(test)]
117impl PartialEq for FolderNode {
118 fn eq(&self, other: &FolderNode) -> bool {
119 cmp_options(&self.guid, &other.guid)
120 && cmp_options(&self.date_added, &other.date_added)
121 && cmp_options(&self.last_modified, &other.last_modified)
122 && cmp_options(&self.title, &other.title)
123 && self.children == other.children
124 }
125}
126
127#[derive(Debug)]
128#[cfg_attr(test, derive(PartialEq))]
129pub enum BookmarkTreeNode {
130 Bookmark { b: BookmarkNode },
131 Separator { s: SeparatorNode },
132 Folder { f: FolderNode },
133}
134
135impl BookmarkTreeNode {
136 pub fn node_type(&self) -> BookmarkType {
137 match self {
138 BookmarkTreeNode::Bookmark { .. } => BookmarkType::Bookmark,
139 BookmarkTreeNode::Folder { .. } => BookmarkType::Folder,
140 BookmarkTreeNode::Separator { .. } => BookmarkType::Separator,
141 }
142 }
143
144 pub fn guid(&self) -> &SyncGuid {
145 let guid = match self {
146 BookmarkTreeNode::Bookmark { b } => b.guid.as_ref(),
147 BookmarkTreeNode::Folder { f } => f.guid.as_ref(),
148 BookmarkTreeNode::Separator { s } => s.guid.as_ref(),
149 };
150 guid.expect("Missing guid?")
152 }
153
154 pub fn created_modified(&self) -> (Timestamp, Timestamp) {
155 let (created, modified) = match self {
156 BookmarkTreeNode::Bookmark { b } => (b.date_added, b.last_modified),
157 BookmarkTreeNode::Folder { f } => (f.date_added, f.last_modified),
158 BookmarkTreeNode::Separator { s } => (s.date_added, s.last_modified),
159 };
160 (
161 created.unwrap_or_else(Timestamp::now),
162 modified.unwrap_or_else(Timestamp::now),
163 )
164 }
165}
166
167impl Serialize for BookmarkTreeNode {
170 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
171 where
172 S: Serializer,
173 {
174 let mut state = serializer.serialize_struct("BookmarkTreeNode", 2)?;
175 match self {
176 BookmarkTreeNode::Bookmark { b } => {
177 state.serialize_field("type", &BookmarkType::Bookmark)?;
178 state.serialize_field("guid", &b.guid)?;
179 state.serialize_field("date_added", &b.date_added)?;
180 state.serialize_field("last_modified", &b.last_modified)?;
181 state.serialize_field("title", &b.title)?;
182 state.serialize_field("url", &b.url.to_string())?;
183 }
184 BookmarkTreeNode::Separator { s } => {
185 state.serialize_field("type", &BookmarkType::Separator)?;
186 state.serialize_field("guid", &s.guid)?;
187 state.serialize_field("date_added", &s.date_added)?;
188 state.serialize_field("last_modified", &s.last_modified)?;
189 }
190 BookmarkTreeNode::Folder { f } => {
191 state.serialize_field("type", &BookmarkType::Folder)?;
192 state.serialize_field("guid", &f.guid)?;
193 state.serialize_field("date_added", &f.date_added)?;
194 state.serialize_field("last_modified", &f.last_modified)?;
195 state.serialize_field("title", &f.title)?;
196 state.serialize_field("children", &f.children)?;
197 }
198 };
199 state.end()
200 }
201}
202
203impl<'de> Deserialize<'de> for BookmarkTreeNode {
204 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
205 where
206 D: Deserializer<'de>,
207 {
208 #[derive(Debug, Default, Deserialize)]
210 #[serde(default)]
211 struct Mapping {
212 #[serde(rename = "type")]
213 bookmark_type: u8,
214 guid: Option<SyncGuid>,
215 date_added: Option<Timestamp>,
216 last_modified: Option<Timestamp>,
217 title: Option<String>,
218 url: Option<String>,
219 children: Vec<BookmarkTreeNode>,
220 }
221 let m = Mapping::deserialize(deserializer)?;
222
223 let url = m.url.as_ref().and_then(|u| match Url::parse(u) {
224 Err(e) => {
225 warn!(
226 "ignoring invalid url for {}: {:?}",
227 m.guid.as_ref().map(AsRef::as_ref).unwrap_or("<no guid>"),
228 e
229 );
230 None
231 }
232 Ok(parsed) => Some(parsed),
233 });
234
235 let bookmark_type = BookmarkType::from_u8_with_valid_url(m.bookmark_type, || url.is_some());
236 Ok(match bookmark_type {
237 BookmarkType::Bookmark => BookmarkNode {
238 guid: m.guid,
239 date_added: m.date_added,
240 last_modified: m.last_modified,
241 title: m.title,
242 url: url.unwrap(),
243 }
244 .into(),
245 BookmarkType::Separator => SeparatorNode {
246 guid: m.guid,
247 date_added: m.date_added,
248 last_modified: m.last_modified,
249 }
250 .into(),
251 BookmarkType::Folder => FolderNode {
252 guid: m.guid,
253 date_added: m.date_added,
254 last_modified: m.last_modified,
255 title: m.title,
256 children: m.children,
257 }
258 .into(),
259 })
260 }
261}
262
263impl From<BookmarkTreeNode> for InsertableItem {
264 fn from(node: BookmarkTreeNode) -> Self {
265 match node {
266 BookmarkTreeNode::Bookmark { b } => InsertableBookmark {
267 parent_guid: SyncGuid::empty(),
268 position: BookmarkPosition::Append,
269 date_added: b.date_added,
270 last_modified: b.last_modified,
271 guid: b.guid,
272 url: b.url,
273 title: b.title,
274 }
275 .into(),
276 BookmarkTreeNode::Separator { s } => InsertableSeparator {
277 parent_guid: SyncGuid::empty(),
278 position: BookmarkPosition::Append,
279 date_added: s.date_added,
280 last_modified: s.last_modified,
281 guid: s.guid,
282 }
283 .into(),
284 BookmarkTreeNode::Folder { f } => InsertableFolder {
285 parent_guid: SyncGuid::empty(),
286 position: BookmarkPosition::Append,
287 date_added: f.date_added,
288 last_modified: f.last_modified,
289 guid: f.guid,
290 title: f.title,
291 children: f.children.into_iter().map(Into::into).collect(),
292 }
293 .into(),
294 }
295 }
296}
297
298#[cfg(test)]
299mod test_serialize {
300 use super::*;
301 use serde_json::json;
302
303 #[test]
304 fn test_tree_serialize() -> Result<()> {
305 let guid = SyncGuid::random();
306 let tree = BookmarkTreeNode::Folder {
307 f: FolderNode {
308 guid: Some(guid.clone()),
309 date_added: None,
310 last_modified: None,
311 title: None,
312 children: vec![BookmarkTreeNode::Bookmark {
313 b: BookmarkNode {
314 guid: None,
315 date_added: None,
316 last_modified: None,
317 title: Some("the bookmark".into()),
318 url: Url::parse("https://www.example.com")?,
319 },
320 }],
321 },
322 };
323 let json = serde_json::to_string_pretty(&tree)?;
325 let deser: BookmarkTreeNode = serde_json::from_str(&json)?;
326 assert_eq!(tree, deser);
327 let jtree = json!({
330 "type": 2,
331 "guid": &guid,
332 "children" : [
333 {
334 "type": 1,
335 "title": "the bookmark",
336 "url": "https://www.example.com/"
337 }
338 ]
339 });
340 let deser_tree: BookmarkTreeNode = serde_json::from_value(jtree).expect("should deser");
341 assert_eq!(tree, deser_tree);
342 Ok(())
343 }
344
345 #[test]
346 fn test_tree_invalid() {
347 let jtree = json!({
348 "type": 2,
349 "children" : [
350 {
351 "type": 1,
352 "title": "bookmark with invalid URL",
353 "url": "invalid_url"
354 },
355 {
356 "type": 1,
357 "title": "bookmark with missing URL",
358 },
359 {
360 "title": "bookmark with missing type, no URL",
361 },
362 {
363 "title": "bookmark with missing type, valid URL",
364 "url": "http://example.com"
365 },
366
367 ]
368 });
369 let deser_tree: BookmarkTreeNode = serde_json::from_value(jtree).expect("should deser");
370 let folder = match deser_tree {
371 BookmarkTreeNode::Folder { f } => f,
372 _ => panic!("must be a folder"),
373 };
374
375 let children = folder.children;
376 assert_eq!(children.len(), 4);
377
378 assert!(match &children[0] {
379 BookmarkTreeNode::Folder { f } =>
380 f.title == Some("bookmark with invalid URL".to_string()),
381 _ => false,
382 });
383 assert!(match &children[1] {
384 BookmarkTreeNode::Folder { f } =>
385 f.title == Some("bookmark with missing URL".to_string()),
386 _ => false,
387 });
388 assert!(match &children[2] {
389 BookmarkTreeNode::Folder { f } => {
390 f.title == Some("bookmark with missing type, no URL".to_string())
391 }
392 _ => false,
393 });
394 assert!(match &children[3] {
395 BookmarkTreeNode::Bookmark { b } => {
396 b.title == Some("bookmark with missing type, valid URL".to_string())
397 }
398 _ => false,
399 });
400 }
401}
402
403pub fn insert_tree(db: &PlacesDb, tree: FolderNode) -> Result<()> {
404 let parent = tree.guid.expect("inserting a tree without the root guid");
407 let tx = db.begin_transaction()?;
408 for child in tree.children {
409 let mut insertable: InsertableItem = child.into();
410 assert!(
411 insertable.parent_guid().is_empty(),
412 "can't specify a parent inserting a tree"
413 );
414 insertable.set_parent_guid(parent.clone());
415 crate::storage::bookmarks::insert_bookmark_in_tx(db, insertable)?;
416 }
417 crate::storage::delete_pending_temp_tables(db)?;
418 tx.commit()?;
419 Ok(())
420}
421
422fn inflate(
423 parent: &mut BookmarkTreeNode,
424 pseudo_tree: &mut HashMap<SyncGuid, Vec<BookmarkTreeNode>>,
425) {
426 if let BookmarkTreeNode::Folder { f: parent } = parent {
427 if let Some(children) = parent
428 .guid
429 .as_ref()
430 .and_then(|guid| pseudo_tree.remove(guid))
431 {
432 parent.children = children;
433 for child in &mut parent.children {
434 inflate(child, pseudo_tree);
435 }
436 }
437 }
438}
439
440#[derive(Debug)]
441struct FetchedTreeRow {
442 level: u32,
443 _id: RowId,
444 guid: SyncGuid,
445 _parent: Option<RowId>,
448 parent_guid: Option<SyncGuid>,
449 node_type: BookmarkType,
450 position: u32,
451 title: Option<String>,
452 date_added: Timestamp,
453 last_modified: Timestamp,
454 url: Option<String>,
455}
456
457impl FetchedTreeRow {
458 pub fn from_row(row: &Row<'_>) -> Result<Self> {
459 let url = row.get::<_, Option<String>>("url")?;
460 Ok(Self {
461 level: row.get("level")?,
462 _id: row.get::<_, RowId>("id")?,
463 guid: row.get::<_, String>("guid")?.into(),
464 _parent: row.get::<_, Option<RowId>>("parent")?,
465 parent_guid: row
466 .get::<_, Option<String>>("parentGuid")?
467 .map(SyncGuid::from),
468 node_type: BookmarkType::from_u8_with_valid_url(row.get::<_, u8>("type")?, || {
469 url.is_some()
470 }),
471 position: row.get("position")?,
472 title: row.get::<_, Option<String>>("title")?,
473 date_added: row.get("dateAdded")?,
474 last_modified: row.get("lastModified")?,
475 url,
476 })
477 }
478}
479
480pub enum FetchDepth {
484 Specific(usize),
485 Deepest,
486}
487
488pub fn fetch_tree(
489 db: &PlacesDb,
490 item_guid: &SyncGuid,
491 target_depth: &FetchDepth,
492) -> Result<Option<(BookmarkTreeNode, Option<SyncGuid>, u32)>> {
493 let sql = r#"
496 WITH RECURSIVE
497 descendants(fk, level, type, id, guid, parent, parentGuid, position,
498 title, dateAdded, lastModified) AS (
499 SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
500 (SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
501 b1.position, b1.title, b1.dateAdded, b1.lastModified
502 FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
503 UNION ALL
504 SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
505 descendants.guid, b2.position, b2.title, b2.dateAdded,
506 b2.lastModified
507 FROM moz_bookmarks b2
508 JOIN descendants ON b2.parent = descendants.id) -- AND b2.id <> :tags_folder)
509 SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
510 d.position, NULLIF(d.title, '') AS title, d.dateAdded,
511 d.lastModified, h.url
512-- (SELECT icon_url FROM moz_icons i
513-- JOIN moz_icons_to_pages ON icon_id = i.id
514-- JOIN moz_pages_w_icons pi ON page_id = pi.id
515-- WHERE pi.page_url_hash = hash(h.url) AND pi.page_url = h.url
516-- ORDER BY width DESC LIMIT 1) AS iconuri,
517-- (SELECT GROUP_CONCAT(t.title, ',')
518-- FROM moz_bookmarks b2
519-- JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder
520-- WHERE b2.fk = h.id
521-- ) AS tags,
522-- EXISTS (SELECT 1 FROM moz_items_annos
523-- WHERE item_id = d.id LIMIT 1) AS has_annos,
524-- (SELECT a.content FROM moz_annos a
525-- JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
526-- WHERE place_id = h.id AND n.name = :charset_anno
527-- ) AS charset
528 FROM descendants d
529 LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
530 LEFT JOIN moz_places h ON h.id = d.fk
531 ORDER BY d.level, d.parent, d.position"#;
532
533 let scope = db.begin_interrupt_scope()?;
534
535 let mut stmt = db.conn().prepare(sql)?;
536
537 let mut results =
538 stmt.query_and_then(&[(":item_guid", item_guid)], FetchedTreeRow::from_row)?;
539
540 let parent_guid: Option<SyncGuid>;
541 let position: u32;
542
543 let mut root = match results.next() {
545 Some(result) => {
546 let row = result?;
547 parent_guid = row.parent_guid.clone();
548 position = row.position;
549 match row.node_type {
550 BookmarkType::Folder => FolderNode {
551 guid: Some(row.guid.clone()),
552 date_added: Some(row.date_added),
553 last_modified: Some(row.last_modified),
554 title: row.title,
555 children: Vec::new(),
556 }
557 .into(),
558 BookmarkType::Bookmark => {
559 match row.url {
561 Some(str_val) => match Url::parse(str_val.as_str()) {
562 Err(_) => return Ok(None),
565 Ok(url) => BookmarkNode {
566 guid: Some(row.guid.clone()),
567 date_added: Some(row.date_added),
568 last_modified: Some(row.last_modified),
569 title: row.title,
570 url,
571 }
572 .into(),
573 },
574 None => {
580 error_support::report_error!(
581 "places-bookmark-corruption",
582 "bookmark {:#} has missing url",
583 row.guid
584 );
585 return Ok(None);
586 }
587 }
588 }
589 BookmarkType::Separator => SeparatorNode {
590 guid: Some(row.guid.clone()),
591 date_added: Some(row.date_added),
592 last_modified: Some(row.last_modified),
593 }
594 .into(),
595 }
596 }
597 None => return Ok(None),
598 };
599
600 if let BookmarkTreeNode::Bookmark { .. } | BookmarkTreeNode::Separator { .. } = root {
602 return Ok(Some((root, parent_guid, position)));
603 }
604
605 scope.err_if_interrupted()?;
606 let mut pseudo_tree: HashMap<SyncGuid, Vec<BookmarkTreeNode>> = HashMap::new();
611 for result in results {
612 let row = result?;
613 scope.err_if_interrupted()?;
614 if let FetchDepth::Specific(d) = *target_depth {
616 if row.level as usize > d + 1 {
617 break;
618 }
619 }
620 let node = match row.node_type {
621 BookmarkType::Bookmark => match &row.url {
622 Some(url_str) => match Url::parse(url_str) {
623 Ok(url) => BookmarkNode {
624 guid: Some(row.guid.clone()),
625 date_added: Some(row.date_added),
626 last_modified: Some(row.last_modified),
627 title: row.title.clone(),
628 url,
629 }
630 .into(),
631 Err(e) => {
632 warn!(
633 "ignoring malformed bookmark {} - invalid URL: {:?}",
634 row.guid, e
635 );
636 continue;
637 }
638 },
639 None => {
640 warn!("ignoring malformed bookmark {} - no URL", row.guid);
641 continue;
642 }
643 },
644 BookmarkType::Separator => SeparatorNode {
645 guid: Some(row.guid.clone()),
646 date_added: Some(row.date_added),
647 last_modified: Some(row.last_modified),
648 }
649 .into(),
650 BookmarkType::Folder => FolderNode {
651 guid: Some(row.guid.clone()),
652 date_added: Some(row.date_added),
653 last_modified: Some(row.last_modified),
654 title: row.title.clone(),
655 children: Vec::new(),
656 }
657 .into(),
658 };
659 if let Some(parent_guid) = row.parent_guid.as_ref().cloned() {
660 let children = pseudo_tree.entry(parent_guid).or_default();
661 children.push(node);
662 }
663 }
664
665 inflate(&mut root, &mut pseudo_tree);
667 Ok(Some((root, parent_guid, position)))
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::api::places_api::test::new_mem_connection;
674 use crate::storage::bookmarks::BookmarkRootGuid;
675 use crate::tests::{assert_json_tree, assert_json_tree_with_depth};
676 use serde_json::json;
677
678 #[test]
681 fn test_fetch_root() -> Result<()> {
682 let conn = new_mem_connection();
683
684 let (t, _, _) =
686 fetch_tree(&conn, &BookmarkRootGuid::Root.into(), &FetchDepth::Deepest)?.unwrap();
687 let f = match t {
688 BookmarkTreeNode::Folder { ref f } => f,
689 _ => panic!("tree root must be a folder"),
690 };
691 assert_eq!(f.guid, Some(BookmarkRootGuid::Root.into()));
692 assert_eq!(f.children.len(), 4);
693 Ok(())
694 }
695
696 #[test]
697 fn test_insert_tree_and_fetch_level() -> Result<()> {
698 let conn = new_mem_connection();
699
700 let tree = FolderNode {
701 guid: Some(BookmarkRootGuid::Unfiled.into()),
702 children: vec![
703 BookmarkNode {
704 guid: None,
705 date_added: None,
706 last_modified: None,
707 title: Some("the bookmark".into()),
708 url: Url::parse("https://www.example.com")?,
709 }
710 .into(),
711 FolderNode {
712 title: Some("A folder".into()),
713 children: vec![
714 BookmarkNode {
715 guid: None,
716 date_added: None,
717 last_modified: None,
718 title: Some("bookmark 1 in A folder".into()),
719 url: Url::parse("https://www.example2.com")?,
720 }
721 .into(),
722 BookmarkNode {
723 guid: None,
724 date_added: None,
725 last_modified: None,
726 title: Some("bookmark 2 in A folder".into()),
727 url: Url::parse("https://www.example3.com")?,
728 }
729 .into(),
730 ],
731 ..Default::default()
732 }
733 .into(),
734 BookmarkNode {
735 guid: None,
736 date_added: None,
737 last_modified: None,
738 title: Some("another bookmark".into()),
739 url: Url::parse("https://www.example4.com")?,
740 }
741 .into(),
742 ],
743 ..Default::default()
744 };
745 insert_tree(&conn, tree)?;
746
747 let expected = json!({
748 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
749 "children": [
750 {
751 "title": "the bookmark",
752 "url": "https://www.example.com/"
753 },
754 {
755 "title": "A folder",
756 "children": [
757 {
758 "title": "bookmark 1 in A folder",
759 "url": "https://www.example2.com/"
760 },
761 {
762 "title": "bookmark 2 in A folder",
763 "url": "https://www.example3.com/"
764 }
765 ],
766 },
767 {
768 "title": "another bookmark",
769 "url": "https://www.example4.com/",
770 }
771 ]
772 });
773 assert_json_tree(&conn, &BookmarkRootGuid::Unfiled.into(), expected.clone());
775
776 assert_json_tree_with_depth(
778 &conn,
779 &BookmarkRootGuid::Unfiled.into(),
780 expected,
781 &FetchDepth::Specific(1),
782 );
783
784 assert_json_tree_with_depth(
786 &conn,
787 &BookmarkRootGuid::Unfiled.into(),
788 json!({
789 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
790 "children": [
791 {
792 "title": "the bookmark",
793 "url": "https://www.example.com/"
794 },
795 {
796 "title": "A folder",
797 "children": [],
798 },
799 {
800 "title": "another bookmark",
801 "url": "https://www.example4.com/",
802 }
803 ]
804 }),
805 &FetchDepth::Specific(0),
806 );
807
808 Ok(())
809 }
810}