From 91f5fd48f85c415c503ca230d0e1f78de9f190a6 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 30 May 2023 13:37:54 +0000 Subject: [PATCH] Bug 1833427 - Add bookmark import from HTML file in new migration wizard. r=fluent-reviewers,kpatenio,flod Original patch by Tim Giles , completed by Mike Conley . Differential Revision: https://phabricator.services.mozilla.com/D179273 --- browser/app/profile/firefox.js | 1 + .../migration/FileMigrators.sys.mjs | 88 ++++++++ .../migration/MigrationUtils.sys.mjs | 3 + .../content/migration-wizard-constants.mjs | 1 + .../migration/content/migration-wizard.mjs | 6 + .../browser/browser_no_browsers_state.js | 7 +- .../tests/unit/bookmarks.exported.html | 32 +++ .../tests/unit/bookmarks.exported.json | 194 ++++++++++++++++++ .../tests/unit/test_BookmarksFileMigrator.js | 120 +++++++++++ .../migration/tests/unit/xpcshell.ini | 3 + .../locales/en-US/browser/migrationWizard.ftl | 29 +++ 11 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 browser/components/migration/tests/unit/bookmarks.exported.html create mode 100644 browser/components/migration/tests/unit/bookmarks.exported.json create mode 100644 browser/components/migration/tests/unit/test_BookmarksFileMigrator.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 36ef416d6894..faa64abcf8ba 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -2166,6 +2166,7 @@ pref("dom.ipc.processPrelaunch.enabled", false); pref("dom.ipc.processPrelaunch.enabled", true); #endif +pref("browser.migrate.bookmarks-file.enabled", true); pref("browser.migrate.brave.enabled", true); pref("browser.migrate.canary.enabled", true); diff --git a/browser/components/migration/FileMigrators.sys.mjs b/browser/components/migration/FileMigrators.sys.mjs index 47e4bc2b35f0..27b8e9a61833 100644 --- a/browser/components/migration/FileMigrators.sys.mjs +++ b/browser/components/migration/FileMigrators.sys.mjs @@ -6,6 +6,8 @@ const lazy = {}; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; ChromeUtils.defineESModuleGetters(lazy, { + BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", + BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs", LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs", MigrationWizardConstants: "chrome://browser/content/migration/migration-wizard-constants.mjs", @@ -239,3 +241,89 @@ export class PasswordFileMigrator extends FileMigratorBase { }; } } + +/** + * A file migrator for importing bookmarks from a HTML or JSON file. + * + * @class BookmarksFileMigrator + * @augments {FileMigratorBase} + */ +export class BookmarksFileMigrator extends FileMigratorBase { + static get key() { + return "file-bookmarks"; + } + + static get displayNameL10nID() { + return "migration-wizard-migrator-display-name-file-bookmarks"; + } + + static get brandImage() { + return "chrome://branding/content/document.ico"; + } + + get enabled() { + return Services.prefs.getBoolPref( + "browser.migrate.bookmarks-file.enabled", + false + ); + } + + get displayedResourceTypes() { + return [ + lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE, + ]; + } + + get progressHeaderL10nID() { + return "migration-bookmarks-from-file-progress-header"; + } + + get successHeaderL10nID() { + return "migration-bookmarks-from-file-success-header"; + } + + async getFilePickerConfig() { + let [title, htmlFilterTitle, jsonFilterTitle] = + await lazy.gFluentStrings.formatValues([ + { id: "migration-bookmarks-from-file-picker-title" }, + { id: "migration-bookmarks-from-file-html-filter-title" }, + { id: "migration-bookmarks-from-file-json-filter-title" }, + ]); + + return { + title, + filters: [ + { + title: htmlFilterTitle, + extensionPattern: "*.html", + }, + { + title: jsonFilterTitle, + extensionPattern: "*.json", + }, + ], + }; + } + + async migrate(filePath) { + let pathCheck = filePath.toLowerCase(); + let importedCount; + + if (pathCheck.endsWith("html")) { + importedCount = await lazy.BookmarkHTMLUtils.importFromFile(filePath); + } else if (pathCheck.endsWith("json") || pathCheck.endsWith("jsonlz4")) { + importedCount = await lazy.BookmarkJSONUtils.importFromFile(filePath); + } + let importedMessage = await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-new-bookmarks", + { + newEntries: importedCount, + } + ); + return { + [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE]: importedMessage, + }; + } +} diff --git a/browser/components/migration/MigrationUtils.sys.mjs b/browser/components/migration/MigrationUtils.sys.mjs index 87dd2e71dc38..aa362b691bd0 100644 --- a/browser/components/migration/MigrationUtils.sys.mjs +++ b/browser/components/migration/MigrationUtils.sys.mjs @@ -112,6 +112,9 @@ const FILE_MIGRATOR_MODULES = Object.freeze({ PasswordFileMigrator: { moduleURI: "resource:///modules/FileMigrators.sys.mjs", }, + BookmarksFileMigrator: { + moduleURI: "resource:///modules/FileMigrators.sys.mjs", + }, }); /** diff --git a/browser/components/migration/content/migration-wizard-constants.mjs b/browser/components/migration/content/migration-wizard-constants.mjs index d0b70c784490..30e1af3d79a3 100644 --- a/browser/components/migration/content/migration-wizard-constants.mjs +++ b/browser/components/migration/content/migration-wizard-constants.mjs @@ -60,6 +60,7 @@ export const MigrationWizardConstants = Object.freeze({ PASSWORDS_FROM_FILE: "PASSWORDS_FROM_FILE", PASSWORDS_NEW: "PASSWORDS_NEW", PASSWORDS_UPDATED: "PASSWORDS_UPDATED", + BOOKMARKS_FROM_FILE: "BOOKMARKS_FROM_FILE", }), /** diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs index 41652cfb71b4..0ac5b9d0a7df 100644 --- a/browser/components/migration/content/migration-wizard.mjs +++ b/browser/components/migration/content/migration-wizard.mjs @@ -158,6 +158,12 @@ export class MigrationWizard extends HTMLElement {   + +
+ + +   +
diff --git a/browser/components/migration/tests/browser/browser_no_browsers_state.js b/browser/components/migration/tests/browser/browser_no_browsers_state.js index 891f3cfd0c7e..cd4677f31dc8 100644 --- a/browser/components/migration/tests/browser/browser_no_browsers_state.js +++ b/browser/components/migration/tests/browser/browser_no_browsers_state.js @@ -51,8 +51,13 @@ add_task(async function test_browser_no_programs() { ); }); + // Now disable all file migrators to make sure that the "Import from file" + // button is hidden. await SpecialPowers.pushPrefEnv({ - set: [["signon.management.page.fileImport.enabled", false]], + set: [ + ["signon.management.page.fileImport.enabled", false], + ["browser.migrate.bookmarks-file.enabled", false], + ], }); await withMigrationWizardDialog(async prefsWin => { diff --git a/browser/components/migration/tests/unit/bookmarks.exported.html b/browser/components/migration/tests/unit/bookmarks.exported.html new file mode 100644 index 000000000000..5a9ec43325b7 --- /dev/null +++ b/browser/components/migration/tests/unit/bookmarks.exported.html @@ -0,0 +1,32 @@ + + + + +Bookmarks +

Bookmarks Menu

+ +

+

Mozilla Firefox

+

+

Help and Tutorials +
Customize Firefox +
Get Involved +
About Us +

+


test

+

+

test post keyword +

+

Bookmarks Toolbar

+

+

Getting Started +
Latest Headlines +

+

Other Bookmarks

+

+

Example.tld +

+

diff --git a/browser/components/migration/tests/unit/bookmarks.exported.json b/browser/components/migration/tests/unit/bookmarks.exported.json new file mode 100644 index 000000000000..2a73f00b312a --- /dev/null +++ b/browser/components/migration/tests/unit/bookmarks.exported.json @@ -0,0 +1,194 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1685116351936000, + "lastModified": 1685372151518000, + "id": 1, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "menu", + "index": 0, + "dateAdded": 1685116351936000, + "lastModified": 1685116352325000, + "id": 2, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "jCs_9YrgXKq7", + "title": "Firefox Nightly Resources", + "index": 0, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 7, + "typeCode": 2, + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "xwdRLsUWYFwm", + "title": "Firefox Nightly blog", + "index": 0, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 8, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://blog.nightly.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://blog.nightly.mozilla.org/" + }, + { + "guid": "uhdiDrWjH0-n", + "title": "Mozilla Bug Tracker", + "index": 1, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 9, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://bugzilla.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://bugzilla.mozilla.org/", + "keyword": "bz", + "postData": null + }, + { + "guid": "zOK7d-gjJ5Vy", + "title": "Mozilla Developer Network", + "index": 2, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 10, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://developer.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://developer.mozilla.org/", + "keyword": "mdn", + "postData": null + }, + { + "guid": "7gcb4320A_y6", + "title": "Nightly Tester Tools", + "index": 3, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 11, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://addons.mozilla.org/firefox/addon/nightly-tester-tools/", + "type": "text/x-moz-place", + "uri": "https://addons.mozilla.org/firefox/addon/nightly-tester-tools/" + }, + { + "guid": "c4753lDvJwNE", + "title": "All your crashes", + "index": 4, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 12, + "typeCode": 1, + "iconUri": "fake-favicon-uri:about:crashes", + "type": "text/x-moz-place", + "uri": "about:crashes" + }, + { + "guid": "IyYGIH9VCs2t", + "title": "Planet Mozilla", + "index": 5, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 13, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://planet.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://planet.mozilla.org/" + } + ] + } + ] + }, + { + "guid": "toolbar_____", + "title": "toolbar", + "index": 1, + "dateAdded": 1685116351936000, + "lastModified": 1685372151518000, + "id": 3, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "5jN1vdzOEnHx", + "title": "Get Involved", + "index": 0, + "dateAdded": 1685116352413000, + "lastModified": 1685116352413000, + "id": 14, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global", + "type": "text/x-moz-place", + "uri": "https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global" + }, + { + "guid": "5RsMT9sWsmIe", + "title": "Why More Psychiatrists Think Mindfulness Can Help Treat ADHD", + "index": 1, + "dateAdded": 1685372143048000, + "lastModified": 1685372143048000, + "id": 15, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/explore/item/why-more-psychiatrists-think-mindfulness-can-help-treat-adhd?utm_source=pocket-newtab" + }, + { + "guid": "ejoNUqAfEMQL", + "title": "Your New Favorite Weeknight Recipe Is Meat-Free (and Easy, Too)", + "index": 2, + "dateAdded": 1685372148200000, + "lastModified": 1685372148200000, + "id": 16, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/collections/your-new-favorite-weeknight-recipe-is-meat-free-and-easy-too?utm_source=pocket-newtab" + }, + { + "guid": "O5QCiQ1zrqHY", + "title": "8 Natural Ways to Repel Insects Without Bug Spray", + "index": 3, + "dateAdded": 1685372151518000, + "lastModified": 1685372151518000, + "id": 17, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/explore/item/8-natural-ways-to-repel-insects-without-bug-spray?utm_source=pocket-newtab" + } + ] + }, + { + "guid": "unfiled_____", + "title": "unfiled", + "index": 3, + "dateAdded": 1685116351936000, + "lastModified": 1685116352272000, + "id": 5, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder" + }, + { + "guid": "mobile______", + "title": "mobile", + "index": 4, + "dateAdded": 1685116351968000, + "lastModified": 1685116352272000, + "id": 6, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "mobileFolder" + } + ] +} diff --git a/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js new file mode 100644 index 000000000000..a30221b5bdbf --- /dev/null +++ b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { BookmarksFileMigrator } = ChromeUtils.importESModule( + "resource:///modules/FileMigrators.sys.mjs" +); + +const { MigrationWizardConstants } = ChromeUtils.importESModule( + "chrome://browser/content/migration/migration-wizard-constants.mjs" +); + +/** + * Tests that the BookmarksFileMigrator properly subclasses FileMigratorBase + * and delegates to BookmarkHTMLUtils or BookmarkJSONUtils. + * + * Normally, we'd override the BookmarkHTMLUtils and BookmarkJSONUtils methods + * in our test here so that we just ensure that they're called with the + * right arguments, rather than testing their behaviour. Unfortunately, both + * BookmarkHTMLUtils and BookmarkJSONUtils are frozen with Object.freeze, which + * prevents sinon from stubbing out any of their methods. Rather than unfreezing + * those objects just for testing, we test the whole flow end-to-end, including + * the import to Places. + */ + +add_setup(() => { + Services.prefs.setBoolPref("browser.migrate.bookmarks-file.enabled", true); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + Services.prefs.clearUserPref("browser.migrate.bookmarks-file.enabled"); + }); +}); + +/** + * First check that the BookmarksFileMigrator implements the required parts + * of the parent class. + */ +add_task(async function test_BookmarksFileMigrator_members() { + let migrator = new BookmarksFileMigrator(); + Assert.ok( + migrator.constructor.key, + `BookmarksFileMigrator implements static getter 'key'` + ); + + Assert.ok( + migrator.constructor.displayNameL10nID, + `BookmarksFileMigrator implements static getter 'displayNameL10nID'` + ); + + Assert.ok( + migrator.constructor.brandImage, + `BookmarksFileMigrator implements static getter 'brandImage'` + ); + + Assert.ok( + migrator.progressHeaderL10nID, + `BookmarksFileMigrator implements getter 'progressHeaderL10nID'` + ); + + Assert.ok( + migrator.successHeaderL10nID, + `BookmarksFileMigrator implements getter 'successHeaderL10nID'` + ); + + Assert.ok( + await migrator.getFilePickerConfig(), + `BookmarksFileMigrator implements method 'getFilePickerConfig'` + ); + + Assert.ok( + migrator.displayedResourceTypes, + `BookmarksFileMigrator implements getter 'displayedResourceTypes'` + ); + + Assert.ok(migrator.enabled, `BookmarksFileMigrator is enabled`); +}); + +add_task(async function test_BookmarksFileMigrator_HTML() { + let migrator = new BookmarksFileMigrator(); + const EXPECTED_SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE]: "8 bookmarks", + }; + + const BOOKMARKS_PATH = PathUtils.join( + do_get_cwd().path, + "bookmarks.exported.html" + ); + + let result = await migrator.migrate(BOOKMARKS_PATH); + + Assert.deepEqual( + result, + EXPECTED_SUCCESS_STATE, + "Returned the expected success state." + ); +}); + +add_task(async function test_BookmarksFileMigrator_JSON() { + let migrator = new BookmarksFileMigrator(); + + const EXPECTED_SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE]: "10 bookmarks", + }; + + const BOOKMARKS_PATH = PathUtils.join( + do_get_cwd().path, + "bookmarks.exported.json" + ); + + let result = await migrator.migrate(BOOKMARKS_PATH); + + Assert.deepEqual( + result, + EXPECTED_SUCCESS_STATE, + "Returned the expected success state." + ); +}); diff --git a/browser/components/migration/tests/unit/xpcshell.ini b/browser/components/migration/tests/unit/xpcshell.ini index bdc70ccb7211..7d1cc5201607 100644 --- a/browser/components/migration/tests/unit/xpcshell.ini +++ b/browser/components/migration/tests/unit/xpcshell.ini @@ -8,11 +8,14 @@ prefs = support-files = Library/** AppData/** + bookmarks.exported.html + bookmarks.exported.json [test_360se_bookmarks.js] skip-if = os != "win" [test_360seMigrationUtils.js] run-if = os == "win" +[test_BookmarksFileMigrator.js] [test_ChromeMigrationUtils_path_chromium_snap.js] run-if = os == "linux" [test_Chrome_bookmarks.js] diff --git a/browser/locales/en-US/browser/migrationWizard.ftl b/browser/locales/en-US/browser/migrationWizard.ftl index 472e52a2b418..bb75b2a2f150 100644 --- a/browser/locales/en-US/browser/migrationWizard.ftl +++ b/browser/locales/en-US/browser/migrationWizard.ftl @@ -38,6 +38,7 @@ migration-wizard-migrator-display-name-chromium-edge-beta = Microsoft Edge Beta migration-wizard-migrator-display-name-edge-legacy = Microsoft Edge Legacy migration-wizard-migrator-display-name-firefox = Firefox migration-wizard-migrator-display-name-file-password-csv = Passwords from CSV file +migration-wizard-migrator-display-name-file-bookmarks = Bookmarks from HTML file migration-wizard-migrator-display-name-ie = Microsoft Internet Explorer migration-wizard-migrator-display-name-opera = Opera migration-wizard-migrator-display-name-opera-gx = Opera GX @@ -108,6 +109,34 @@ migration-wizard-progress-success-updated-passwords = *[other] { $updatedEntries } updated } +migration-bookmarks-from-file-picker-title = Import Bookmarks File +migration-bookmarks-from-file-progress-header = Importing Bookmarks +migration-bookmarks-from-file = Bookmarks +migration-bookmarks-from-file-success-header = Bookmarks Imported Successfully + +# A description for the .html file format that may be shown as the file type +# filter by the operating system. +migration-bookmarks-from-file-html-filter-title = + { PLATFORM() -> + [macos] HTML Document + *[other] HTML File + } + +# A description for the .json file format that may be shown as the file type +# filter by the operating system. +migration-bookmarks-from-file-json-filter-title = JSON File + +# Shown in the migration wizard after importing bookmarks from a file +# has completed. +# +# Variables: +# $newEntries (Number): the number of imported bookmarks. +migration-wizard-progress-success-new-bookmarks = + { $newEntries -> + [one] { $newEntries } bookmark + *[other] { $newEntries } bookmarks + } + migration-import-button-label = Import migration-choose-to-import-from-file-button-label = Import From File migration-import-from-file-button-label = Select File