275 lines
8.5 KiB
JavaScript
275 lines
8.5 KiB
JavaScript
/* 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.
|
|
*/
|
|
|
|
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: bookmark.URL.URI,
|
|
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;
|
|
}
|