Source code
Revision control
Copy as Markdown
Other Tools
/* 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,
/*
* 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.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
const PREF_LOGLEVEL = "browser.policies.loglevel";
ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
"resource://gre/modules/Console.sys.mjs"
);
return new ConsoleAPI({
prefix: "BookmarksPolicies",
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.sys.mjs 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) {
if (bookmark.Favicon.protocol != "data:") {
lazy.log.error(
`Pass a valid data: URI for favicon on bookmark "${bookmark.Title}", instead of "${bookmark.Favicon.URI.spec}"`
);
return;
}
lazy.PlacesUtils.favicons
.setFaviconForPage(
bookmark.URL.URI,
Services.io.newURI("fake-favicon-uri:" + bookmark.URL.href),
bookmark.Favicon.URI
)
.catch(lazy.log.error);
}
// 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.
ChromeUtils.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;
}