/* 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; }