From ca99682d13a51fff09393cff20335d5d17f758aa Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Thu, 22 Feb 2018 17:47:50 -0300 Subject: [PATCH] Bug 1428924 - Policy: Allow creation of bookmarks in the Bookmarks toolbar, Menu, or a folder inside them. r=mak MozReview-Commit-ID: 2k2Cl10AV9C --HG-- extra : rebase_source : 803c7f88d64159e3ba1627a53ee8a3a24f758b84 --- .../enterprisepolicies/Policies.jsm | 10 + .../helpers/BookmarksPolicies.jsm | 303 ++++++++++++++++++ .../enterprisepolicies/helpers/moz.build | 4 + .../helpers/sample_bookmarks.json | 37 +++ .../schemas/policies-schema.json | 33 ++ .../tests/browser/browser.ini | 1 + .../tests/browser/browser_policy_bookmarks.js | 262 +++++++++++++++ 7 files changed, 650 insertions(+) create mode 100644 browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm create mode 100644 browser/components/enterprisepolicies/helpers/sample_bookmarks.json create mode 100644 browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js diff --git a/browser/components/enterprisepolicies/Policies.jsm b/browser/components/enterprisepolicies/Policies.jsm index 6391b431c0b4..6bd9bb067827 100644 --- a/browser/components/enterprisepolicies/Policies.jsm +++ b/browser/components/enterprisepolicies/Policies.jsm @@ -10,6 +10,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "gXulStore", "@mozilla.org/xul/xulstore;1", "nsIXULStore"); +XPCOMUtils.defineLazyModuleGetters(this, { + BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.jsm", +}); + const PREF_LOGLEVEL = "browser.policies.loglevel"; const PREF_MENU_ALREADY_DISPLAYED = "browser.policies.menuBarWasDisplayed"; const BROWSER_DOCUMENT_URL = "chrome://browser/content/browser.xul"; @@ -69,6 +73,12 @@ this.Policies = { } }, + "Bookmarks": { + onBeforeUIStartup(manager, param) { + BookmarksPolicies.processBookmarks(param); + } + }, + "Cookies": { onBeforeUIStartup(manager, param) { addAllowDenyPermissions("cookie", param.Allow, param.Block); diff --git a/browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm new file mode 100644 index 000000000000..75f8d99ad5cf --- /dev/null +++ b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.jsm @@ -0,0 +1,303 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* + * A Bookmark object received through the policy engine will be an + * object with the following properties: + * + * - URL (nsIURI) + * (required) The URL for this bookmark + * + * - Title (string) + * (required) The title for this bookmark + * + * - Placement (string) + * (optional) Either "toolbar" or "menu". If missing or invalid, + * "toolbar" will be used + * + * - Folder (string) + * (optional) The name of the folder to put this bookmark into. + * If present, a folder with this name will be created in the + * chosen placement above, and the bookmark will be created there. + * If missing, the bookmark will be created directly into the + * chosen placement. + * + * - Favicon (nsIURI) + * (optional) An http:, https: or data: URL with the favicon. + * If possible, we recommend against using this property, in order + * to keep the json file small. + * If a favicon is not provided through the policy, it will be loaded + * naturally after the user first visits the bookmark. + * + * + * Note: The Policy Engine automatically converts the strings given to + * the URL and favicon properties into a nsIURI object. + * + * The schema for this object is defined in policies-schema.json. + */ + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +const PREF_LOGLEVEL = "browser.policies.loglevel"; + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {}); + return new ConsoleAPI({ + prefix: "BookmarksPolicies.jsm", + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.jsm for details. + maxLogLevel: "error", + maxLogLevelPref: PREF_LOGLEVEL, + }); +}); + +this.EXPORTED_SYMBOLS = [ "BookmarksPolicies" ]; + +this.BookmarksPolicies = { + // These prefixes must only contain characters + // allowed by PlacesUtils.isValidGuid + BOOKMARK_GUID_PREFIX: "PolB-", + FOLDER_GUID_PREFIX: "PolF-", + + /* + * Process the bookmarks specified by the policy engine. + * + * @param param + * This will be an array of bookmarks objects, as + * described on the top of this file. + */ + processBookmarks(param) { + calculateLists(param).then(async function addRemoveBookmarks(results) { + for (let bookmark of results.add.values()) { + await insertBookmark(bookmark).catch(log.error); + } + for (let bookmark of results.remove.values()) { + await PlacesUtils.bookmarks.remove(bookmark).catch(log.error); + } + for (let bookmark of results.emptyFolders.values()) { + await PlacesUtils.bookmarks.remove(bookmark).catch(log.error); + } + + gFoldersMapPromise.then(map => map.clear()); + }); + } +}; + +/* + * This function calculates the differences between the existing bookmarks + * that are managed by the policy engine (which are known through a guid + * prefix) and the specified bookmarks in the policy file. + * They can differ if the policy file has changed. + * + * @param specifiedBookmarks + * This will be an array of bookmarks objects, as + * described on the top of this file. + */ +async function calculateLists(specifiedBookmarks) { + // --------- STEP 1 --------- + // Build two Maps (one with the existing bookmarks, another with + // the specified bookmarks), to make iteration quicker. + + // LIST A + // MAP of url (string) -> bookmarks objects from the Policy Engine + let specifiedBookmarksMap = new Map(); + for (let bookmark of specifiedBookmarks) { + specifiedBookmarksMap.set(bookmark.URL.spec, bookmark); + } + + // LIST B + // MAP of url (string) -> bookmarks objects from Places + let existingBookmarksMap = new Map(); + await PlacesUtils.bookmarks.fetch( + { guidPrefix: BookmarksPolicies.BOOKMARK_GUID_PREFIX }, + (bookmark) => existingBookmarksMap.set(bookmark.url.href, bookmark) + ); + + // --------- STEP 2 --------- + // + // /=====/====\=====\ + // / / \ \ + // | | | | + // | A | {} | B | + // | | | | + // \ \ / / + // \=====\====/=====/ + // + // Find the intersection of the two lists. Items in the intersection + // are removed from the original lists. + // + // The items remaining in list A are new bookmarks to be added. + // The items remaining in list B are old bookmarks to be removed. + // + // There's nothing to do with items in the intersection, so there's no + // need to keep track of them. + // + // BONUS: It's necessary to keep track of the folder names that were + // seen, to make sure we remove the ones that were left empty. + + let foldersSeen = new Set(); + + for (let [url, item] of specifiedBookmarksMap) { + foldersSeen.add(item.Folder); + + if (existingBookmarksMap.has(url)) { + log.debug(`Bookmark intersection: ${url}`); + // If this specified bookmark exists in the existing bookmarks list, + // we can remove it from both lists as it's in the intersection. + specifiedBookmarksMap.delete(url); + existingBookmarksMap.delete(url); + } + } + + for (let url of specifiedBookmarksMap.keys()) { + log.debug(`Bookmark to add: ${url}`); + } + + for (let url of existingBookmarksMap.keys()) { + log.debug(`Bookmark to remove: ${url}`); + } + + // SET of folders to be deleted (bookmarks object from Places) + let foldersToRemove = new Set(); + + // If no bookmarks will be deleted, then no folder will + // need to be deleted either, so this next section can be skipped. + if (existingBookmarksMap.size > 0) { + await PlacesUtils.bookmarks.fetch( + { guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX }, + (folder) => { + if (!foldersSeen.has(folder.title)) { + log.debug(`Folder to remove: ${folder.title}`); + foldersToRemove.add(folder); + } + } + ); + } + + return { + add: specifiedBookmarksMap, + remove: existingBookmarksMap, + emptyFolders: foldersToRemove + }; +} + +async function insertBookmark(bookmark) { + let parentGuid = await getParentGuid(bookmark.Placement, + bookmark.Folder); + + await PlacesUtils.bookmarks.insert({ + url: bookmark.URL, + title: bookmark.Title, + guid: generateGuidWithPrefix(BookmarksPolicies.BOOKMARK_GUID_PREFIX), + parentGuid, + }); + + if (bookmark.Favicon) { + await setFaviconForBookmark(bookmark).catch( + () => log.error(`Error setting favicon for ${bookmark.Title}`)); + } +} + +async function setFaviconForBookmark(bookmark) { + let faviconURI; + let nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); + + switch (bookmark.Favicon.scheme) { + case "data": + // data urls must first call replaceFaviconDataFromDataURL, using a + // fake URL. Later, it's needed to call setAndFetchFaviconForPage + // with the same URL. + faviconURI = Services.io.newURI("fake-favicon-uri:" + bookmark.URL.spec); + + PlacesUtils.favicons.replaceFaviconDataFromDataURL( + faviconURI, + bookmark.Favicon.spec, + 0, /* max expiration length */ + nullPrincipal + ); + break; + + case "http": + case "https": + faviconURI = bookmark.Favicon; + break; + + default: + log.error(`Bad URL given for favicon on bookmark "${bookmark.Title}"`); + return Promise.resolve(); + } + + return new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + bookmark.URL, + faviconURI, + false, /* forceReload */ + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, + nullPrincipal + ); + }); +} + +function generateGuidWithPrefix(prefix) { + // Generates a random GUID and replace its beginning with the given + // prefix. We do this instead of just prepending the prefix to keep + // the correct character length. + return prefix + PlacesUtils.history.makeGuid().substring(prefix.length); +} + +// Cache of folder names to guids to be used by the getParentGuid +// function. The name consists in the parentGuid (which should always +// be the menuGuid or the toolbarGuid) + the folder title. This is to +// support having the same folder name in both the toolbar and menu. +XPCOMUtils.defineLazyGetter(this, "gFoldersMapPromise", () => { + return new Promise(resolve => { + let foldersMap = new Map(); + return PlacesUtils.bookmarks.fetch( + { + guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX + }, + (result) => { + foldersMap.set(`${result.parentGuid}|${result.title}`, result.guid); + } + ).then(() => resolve(foldersMap)); + }); +}); + +async function getParentGuid(placement, folderTitle) { + // Defaults to toolbar if no placement was given. + let parentGuid = (placement == "menu") ? + PlacesUtils.bookmarks.menuGuid : + PlacesUtils.bookmarks.toolbarGuid; + + if (!folderTitle) { + // If no folderTitle is given, this bookmark is to be placed directly + // into the toolbar or menu. + return parentGuid; + } + + let foldersMap = await gFoldersMapPromise; + let folderName = `${parentGuid}|${folderTitle}`; + + if (foldersMap.has(folderName)) { + return foldersMap.get(folderName); + } + + let guid = generateGuidWithPrefix(BookmarksPolicies.FOLDER_GUID_PREFIX); + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: folderTitle, + guid, + parentGuid + }); + + foldersMap.set(folderName, guid); + return guid; +} diff --git a/browser/components/enterprisepolicies/helpers/moz.build b/browser/components/enterprisepolicies/helpers/moz.build index 560d837fc699..3a1b89e5607b 100644 --- a/browser/components/enterprisepolicies/helpers/moz.build +++ b/browser/components/enterprisepolicies/helpers/moz.build @@ -6,3 +6,7 @@ with Files("**"): BUG_COMPONENT = ("Firefox", "Enterprise Policies") + +EXTRA_JS_MODULES.policies += [ + 'BookmarksPolicies.jsm', +] diff --git a/browser/components/enterprisepolicies/helpers/sample_bookmarks.json b/browser/components/enterprisepolicies/helpers/sample_bookmarks.json new file mode 100644 index 000000000000..5393526aab3f --- /dev/null +++ b/browser/components/enterprisepolicies/helpers/sample_bookmarks.json @@ -0,0 +1,37 @@ +{ + "policies": { + "DisplayBookmarksToolbar": true, + + "Bookmarks": [ + { + "Title": "Bookmark 1", + "URL": "https://bookmark1.example.com" + }, + { + "Title": "Bookmark 2", + "URL": "https://bookmark2.example.com", + "Favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwMDAsTBZbkNwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABNElEQVQ4y8WSsU0DURBE3yyWIaAJaqAAN4DPSL6AlIACKIEOyJEgRsIgOOkiInJqgAKowNg7BHdn7MOksNl+zZ//dvbDf5cAiklp22BdVtXdeTEpDYDB9m1VzU6OJuVp2NdEQCaI96fH2YHG4+mDduKYNMYINTcjcGbXzQVDEAphG0k48zUsajIbnAiMIXThpW8EICE0RAK4dvoKg9NIcTiQ589otyHOZLnwqK5nLwBFUZ4igc3iM0d1ff8CMC6mZ6Ihiaqq3gi1aUAnArD00SW1fq5OLBg0ymYmSZsR2/t4e/rGyCLW0sbp3oq+yTYqVgytQWui2FS7XYF7GFprY921T4CNQt8zr47dNzCkIX7y/jBtH+v+RGMQrc828W8pApnZbmEVQp/Ae7BlOy2ttib81/UFc+WRWEbjckIAAAAASUVORK5CYII=", + "Folder": "Folder 1" + }, + { + "Title": "Bookmark 3", + "URL": "https://bookmark3.example.com", + "Favicon": "https://www.mozilla.org/favicon.ico", + "Placement": "menu" + }, + { + "Title": "Bookmark 4", + "URL": "https://bookmark4.example.com", + "Favicon": "https://www.mozilla.org/favicon.ico", + "Folder": "Folder 1" + }, + { + "Title": "Bookmark 5", + "URL": "https://bookmark5.example.com", + "Favicon": "https://www.mozilla.org/favicon.ico", + "Placement": "menu", + "Folder": "Folder 2" + } + ] + } +} \ No newline at end of file diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json index 2517f8cff90b..1b02b35622dd 100644 --- a/browser/components/enterprisepolicies/schemas/policies-schema.json +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -42,6 +42,39 @@ "enum": [true] }, + "Bookmarks": { + "description": "Allows the creation of bookmarks in the Bookmarks bar, Bookmarks menu, or a specified folder inside them.", + "first_available": "60.0", + + "type": "array", + "items": { + "type": "object", + "properties": { + "Title": { + "type": "string" + }, + + "URL": { + "type": "URL" + }, + + "Favicon": { + "type": "URL" + }, + + "Placement": { + "type": "string", + "enum": ["toolbar", "menu"] + }, + + "Folder": { + "type": "string" + } + }, + "required": ["title", "URL"] + } + }, + "Cookies": { "description": "Allow or deny websites to set cookies.", "first_available": "60.0", diff --git a/browser/components/enterprisepolicies/tests/browser/browser.ini b/browser/components/enterprisepolicies/tests/browser/browser.ini index 0032e5896ce3..585e4724050a 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser.ini +++ b/browser/components/enterprisepolicies/tests/browser/browser.ini @@ -18,6 +18,7 @@ support-files = [browser_policy_block_about_profiles.js] [browser_policy_block_about_support.js] [browser_policy_block_set_desktop_background.js] +[browser_policy_bookmarks.js] [browser_policy_default_browser_check.js] [browser_policy_disable_formhistory.js] [browser_policy_disable_fxscreenshots.js] diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js new file mode 100644 index 000000000000..3db7563f5961 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js @@ -0,0 +1,262 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm"); + +const FAVICON_DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwMDAsTBZbkNwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABNElEQVQ4y8WSsU0DURBE3yyWIaAJaqAAN4DPSL6AlIACKIEOyJEgRsIgOOkiInJqgAKowNg7BHdn7MOksNl+zZ//dvbDf5cAiklp22BdVtXdeTEpDYDB9m1VzU6OJuVp2NdEQCaI96fH2YHG4+mDduKYNMYINTcjcGbXzQVDEAphG0k48zUsajIbnAiMIXThpW8EICE0RAK4dvoKg9NIcTiQ589otyHOZLnwqK5nLwBFUZ4igc3iM0d1ff8CMC6mZ6Ihiaqq3gi1aUAnArD00SW1fq5OLBg0ymYmSZsR2/t4e/rGyCLW0sbp3oq+yTYqVgytQWui2FS7XYF7GFprY921T4CNQt8zr47dNzCkIX7y/jBtH+v+RGMQrc828W8pApnZbmEVQp/Ae7BlOy2ttib81/UFc+WRWEbjckIAAAAASUVORK5CYII="; + +const { BookmarksPolicies } = ChromeUtils.import("resource:///modules/policies/BookmarksPolicies.jsm", {}); + +let CURRENT_POLICY; + +const BASE_POLICY = { + "policies": { + "display_bookmarks_toolbar": true, + "Bookmarks": [ + { + "Title": "Bookmark 1", + "URL": "https://bookmark1.example.com/", + "Favicon": FAVICON_DATA + }, + { + "Title": "Bookmark 2", + "URL": "https://bookmark2.example.com/", + "Folder": "Folder 1" + }, + { + "Title": "Bookmark 3", + "URL": "https://bookmark3.example.com/", + "Placement": "menu" + }, + { + "Title": "Bookmark 4", + "URL": "https://bookmark4.example.com/", + "Folder": "Folder 1" + }, + { + "Title": "Bookmark 5", + "URL": "https://bookmark5.example.com/", + "Placement": "menu", + "Folder": "Folder 2" + } + ] + } +}; + +/* + * ================================= + * = HELPER FUNCTIONS FOR THE TEST = + * ================================= + */ +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +function findBookmarkInPolicy(bookmark) { + // Find the entry in the given policy that corresponds + // to this bookmark object from Places. + for (let entry of CURRENT_POLICY.policies.Bookmarks) { + if (entry.Title == bookmark.title) { + return entry; + } + } + return null; +} + +async function promiseAllChangesMade({itemsToAdd, itemsToRemove}) { + return new Promise(resolve => { + let bmObserver = { + onItemAdded() { + itemsToAdd--; + if (itemsToAdd == 0 && itemsToRemove == 0) { + PlacesUtils.bookmarks.removeObserver(bmObserver); + resolve(); + } + }, + + onItemRemoved() { + itemsToRemove--; + if (itemsToAdd == 0 && itemsToRemove == 0) { + PlacesUtils.bookmarks.removeObserver(bmObserver); + resolve(); + } + }, + + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemChanged() {}, + onItemVisited() {}, + onItemMoved() {}, + }; + PlacesUtils.bookmarks.addObserver(bmObserver); + }); +} + +/* + * ================== + * = CHECK FUNCTION = + * ================== + * + * Performs all the checks comparing what was given in + * the policy JSON with what was retrieved from Places. + */ +async function check({expectedNumberOfFolders}) { + let bookmarks = [], folders = []; + + await PlacesUtils.bookmarks.fetch({ guidPrefix: BookmarksPolicies.BOOKMARK_GUID_PREFIX }, (r) => { + bookmarks.push(r); + }); + await PlacesUtils.bookmarks.fetch({ guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX }, (r) => { + folders.push(r); + }); + + let foldersToGuids = new Map(); + + for (let folder of folders) { + is(folder.type, PlacesUtils.bookmarks.TYPE_FOLDER, "Correct type for folder"); + foldersToGuids.set(folder.title, folder.guid); + } + + // For simplification and accuracy purposes, the number of expected + // folders is manually specified in the test. + is(folders.length, expectedNumberOfFolders, "Correct number of folders expected"); + is(foldersToGuids.size, expectedNumberOfFolders, "There aren't two different folders with the same name"); + + is(CURRENT_POLICY.policies.Bookmarks.length, bookmarks.length, "The correct number of bookmarks exist"); + + for (let bookmark of bookmarks) { + is(bookmark.type, PlacesUtils.bookmarks.TYPE_BOOKMARK, "Correct type for bookmark"); + + let entry = findBookmarkInPolicy(bookmark); + + is(bookmark.title, entry.Title, "Title matches"); + is(bookmark.url.href, entry.URL, "URL matches"); + + let expectedPlacementGuid; + if (entry.Folder) { + expectedPlacementGuid = foldersToGuids.get(entry.Folder); + } else { + expectedPlacementGuid = entry.Placement == "menu" ? + PlacesUtils.bookmarks.menuGuid : + PlacesUtils.bookmarks.toolbarGuid; + } + + is(bookmark.parentGuid, expectedPlacementGuid, "Correctly placed"); + } +} + +/* + * ================ + * = ACTUAL TESTS = + * ================ + * + * Note: the order of these tests is important, as we want to test not + * only the end result of each configuration, but also the diff algorithm + * that will add or remove bookmarks depending on how the policy changed. + */ + +add_task(async function test_initial_bookmarks() { + // Make a copy of the policy because we will be adding/removing entries from it + CURRENT_POLICY = deepClone(BASE_POLICY); + + setupPolicyEngineWithJson(CURRENT_POLICY); + + await promiseAllChangesMade({ + itemsToAdd: 7, // 5 bookmarks + 2 folders + itemsToRemove: 0, + }); + + await check({ expectedNumberOfFolders: 2 }); +}); + +add_task(async function checkFavicon() { + let bookmark1url = CURRENT_POLICY.policies.Bookmarks[0].URL; + + let result = await new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage( + Services.io.newURI(bookmark1url), + (uri, _, data) => resolve({uri, data}) + ); + }); + + is(result.uri.spec, "fake-favicon-uri:" + bookmark1url, "Favicon URI is correct"); + // data is an array of octets, which will be a bit hard to compare against + // FAVICON_DATA, which is base64 encoded. Checking the expected length should + // be good indication that this is working properly. + is(result.data.length, 464, "Favicon data has the correct length"); + + let faviconsExpiredNotification = TestUtils.topicObserved("places-favicons-expired"); + PlacesUtils.favicons.expireAllFavicons(); + await faviconsExpiredNotification; +}); + +add_task(async function test_remove_Bookmark_2() { + // Continuing from the previous policy: + // + // Remove the 2nd bookmark. It is inside "Folder 1", but that + // folder won't be empty, so it must not be removed. + CURRENT_POLICY.policies.Bookmarks.splice(2, 1); + + setupPolicyEngineWithJson(CURRENT_POLICY); + + await promiseAllChangesMade({ + itemsToAdd: 0, + itemsToRemove: 1, // 1 bookmark + }); + + await check({ expectedNumberOfFolders: 2 }); +}); + +add_task(async function test_remove_Bookmark_5() { + // Continuing from the previous policy: + // + // Remove the last bookmark in the policy, + // which means the "Folder 2" should also disappear + CURRENT_POLICY.policies.Bookmarks.splice(-1, 1); + + setupPolicyEngineWithJson(CURRENT_POLICY); + + await promiseAllChangesMade({ + itemsToAdd: 0, + itemsToRemove: 2, // 1 bookmark and 1 folder + }); + + await check({ expectedNumberOfFolders: 1 }); +}); + +add_task(async function test_revert_to_original_policy() { + CURRENT_POLICY = deepClone(BASE_POLICY); + + // Reverts to the original policy, which means that: + // - "Bookmark 2" + // - "Bookmark 5" + // - "Folder 2" + // should be recreated + setupPolicyEngineWithJson(CURRENT_POLICY); + + await promiseAllChangesMade({ + itemsToAdd: 3, // 2 bookmarks and 1 folder + itemsToRemove: 0, + }); + + await check({ expectedNumberOfFolders: 2 }); +}); + +// Leave this one at the end, so that it cleans up any +// bookmarks created during this test. +add_task(async function test_empty_all_bookmarks() { + CURRENT_POLICY = { policies: { Bookmarks: [] } }; + + setupPolicyEngineWithJson(CURRENT_POLICY); + + await promiseAllChangesMade({ + itemsToAdd: 0, + itemsToRemove: 7, // 5 bookmarks and 2 folders + }); + + check({ expectedNumberOfFolders: 0 }); +});