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
This commit is contained in:
Felipe Gomes 2018-02-22 17:47:50 -03:00
Родитель 25f5f137e8
Коммит ca99682d13
7 изменённых файлов: 650 добавлений и 0 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -6,3 +6,7 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Enterprise Policies")
EXTRA_JS_MODULES.policies += [
'BookmarksPolicies.jsm',
]

Просмотреть файл

@ -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"
}
]
}
}

Просмотреть файл

@ -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",

Просмотреть файл

@ -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]

Просмотреть файл

@ -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 });
});