summaryrefslogtreecommitdiffstats
path: root/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs')
-rw-r--r--browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs301
1 files changed, 301 insertions, 0 deletions
diff --git a/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
new file mode 100644
index 0000000000..47b706c7b1
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
@@ -0,0 +1,301 @@
+/* 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/. */
+
+/*
+ * A Bookmark object received through the policy engine will be an
+ * object with the following properties:
+ *
+ * - URL (URL)
+ * (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 (URL)
+ * (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 URL object.
+ *
+ * The schema for this object is defined in policies-schema.json.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ 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,
+ });
+});
+
+export const 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(lazy.log.error);
+ }
+ for (let bookmark of results.remove.values()) {
+ await lazy.PlacesUtils.bookmarks.remove(bookmark).catch(lazy.log.error);
+ }
+ for (let bookmark of results.emptyFolders.values()) {
+ await lazy.PlacesUtils.bookmarks.remove(bookmark).catch(lazy.log.error);
+ }
+
+ lazy.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.href, bookmark);
+ }
+
+ // LIST B
+ // MAP of url (string) -> bookmarks objects from Places
+ let existingBookmarksMap = new Map();
+ await lazy.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)) {
+ lazy.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()) {
+ lazy.log.debug(`Bookmark to add: ${url}`);
+ }
+
+ for (let url of existingBookmarksMap.keys()) {
+ lazy.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 lazy.PlacesUtils.bookmarks.fetch(
+ { guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX },
+ folder => {
+ if (!foldersSeen.has(folder.title)) {
+ lazy.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 lazy.PlacesUtils.bookmarks.insert({
+ url: Services.io.newURI(bookmark.URL.href),
+ title: bookmark.Title,
+ guid: lazy.PlacesUtils.generateGuidWithPrefix(
+ BookmarksPolicies.BOOKMARK_GUID_PREFIX
+ ),
+ parentGuid,
+ });
+
+ if (bookmark.Favicon) {
+ setFaviconForBookmark(bookmark);
+ }
+}
+
+function setFaviconForBookmark(bookmark) {
+ let faviconURI;
+ let nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
+
+ switch (bookmark.Favicon.protocol) {
+ 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.href);
+
+ lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+ faviconURI,
+ bookmark.Favicon.href,
+ 0 /* max expiration length */,
+ nullPrincipal
+ );
+ break;
+
+ case "http:":
+ case "https:":
+ faviconURI = Services.io.newURI(bookmark.Favicon.href);
+ break;
+
+ default:
+ lazy.log.error(
+ `Bad URL given for favicon on bookmark "${bookmark.Title}"`
+ );
+ return;
+ }
+
+ lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ Services.io.newURI(bookmark.URL.href),
+ faviconURI,
+ false /* forceReload */,
+ lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ nullPrincipal
+ );
+}
+
+// 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(lazy, "gFoldersMapPromise", () => {
+ return new Promise(resolve => {
+ let foldersMap = new Map();
+ return lazy.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"
+ ? lazy.PlacesUtils.bookmarks.menuGuid
+ : lazy.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 lazy.gFoldersMapPromise;
+ let folderName = `${parentGuid}|${folderTitle}`;
+
+ if (foldersMap.has(folderName)) {
+ return foldersMap.get(folderName);
+ }
+
+ let guid = lazy.PlacesUtils.generateGuidWithPrefix(
+ BookmarksPolicies.FOLDER_GUID_PREFIX
+ );
+ await lazy.PlacesUtils.bookmarks.insert({
+ type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: folderTitle,
+ guid,
+ parentGuid,
+ });
+
+ foldersMap.set(folderName, guid);
+ return guid;
+}