1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7/* 8 * A Bookmark object received through the policy engine will be an 9 * object with the following properties: 10 * 11 * - URL (URL) 12 * (required) The URL for this bookmark 13 * 14 * - Title (string) 15 * (required) The title for this bookmark 16 * 17 * - Placement (string) 18 * (optional) Either "toolbar" or "menu". If missing or invalid, 19 * "toolbar" will be used 20 * 21 * - Folder (string) 22 * (optional) The name of the folder to put this bookmark into. 23 * If present, a folder with this name will be created in the 24 * chosen placement above, and the bookmark will be created there. 25 * If missing, the bookmark will be created directly into the 26 * chosen placement. 27 * 28 * - Favicon (URL) 29 * (optional) An http:, https: or data: URL with the favicon. 30 * If possible, we recommend against using this property, in order 31 * to keep the json file small. 32 * If a favicon is not provided through the policy, it will be loaded 33 * naturally after the user first visits the bookmark. 34 * 35 * 36 * Note: The Policy Engine automatically converts the strings given to 37 * the URL and favicon properties into a URL object. 38 * 39 * The schema for this object is defined in policies-schema.json. 40 */ 41 42ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 43ChromeUtils.import("resource://gre/modules/Services.jsm"); 44 45ChromeUtils.defineModuleGetter(this, "PlacesUtils", 46 "resource://gre/modules/PlacesUtils.jsm"); 47 48const PREF_LOGLEVEL = "browser.policies.loglevel"; 49 50XPCOMUtils.defineLazyGetter(this, "log", () => { 51 let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {}); 52 return new ConsoleAPI({ 53 prefix: "BookmarksPolicies.jsm", 54 // tip: set maxLogLevel to "debug" and use log.debug() to create detailed 55 // messages during development. See LOG_LEVELS in Console.jsm for details. 56 maxLogLevel: "error", 57 maxLogLevelPref: PREF_LOGLEVEL, 58 }); 59}); 60 61this.EXPORTED_SYMBOLS = [ "BookmarksPolicies" ]; 62 63this.BookmarksPolicies = { 64 // These prefixes must only contain characters 65 // allowed by PlacesUtils.isValidGuid 66 BOOKMARK_GUID_PREFIX: "PolB-", 67 FOLDER_GUID_PREFIX: "PolF-", 68 69 /* 70 * Process the bookmarks specified by the policy engine. 71 * 72 * @param param 73 * This will be an array of bookmarks objects, as 74 * described on the top of this file. 75 */ 76 processBookmarks(param) { 77 calculateLists(param).then(async function addRemoveBookmarks(results) { 78 for (let bookmark of results.add.values()) { 79 await insertBookmark(bookmark).catch(log.error); 80 } 81 for (let bookmark of results.remove.values()) { 82 await PlacesUtils.bookmarks.remove(bookmark).catch(log.error); 83 } 84 for (let bookmark of results.emptyFolders.values()) { 85 await PlacesUtils.bookmarks.remove(bookmark).catch(log.error); 86 } 87 88 gFoldersMapPromise.then(map => map.clear()); 89 }); 90 } 91}; 92 93/* 94 * This function calculates the differences between the existing bookmarks 95 * that are managed by the policy engine (which are known through a guid 96 * prefix) and the specified bookmarks in the policy file. 97 * They can differ if the policy file has changed. 98 * 99 * @param specifiedBookmarks 100 * This will be an array of bookmarks objects, as 101 * described on the top of this file. 102 */ 103async function calculateLists(specifiedBookmarks) { 104 // --------- STEP 1 --------- 105 // Build two Maps (one with the existing bookmarks, another with 106 // the specified bookmarks), to make iteration quicker. 107 108 // LIST A 109 // MAP of url (string) -> bookmarks objects from the Policy Engine 110 let specifiedBookmarksMap = new Map(); 111 for (let bookmark of specifiedBookmarks) { 112 specifiedBookmarksMap.set(bookmark.URL.href, bookmark); 113 } 114 115 // LIST B 116 // MAP of url (string) -> bookmarks objects from Places 117 let existingBookmarksMap = new Map(); 118 await PlacesUtils.bookmarks.fetch( 119 { guidPrefix: BookmarksPolicies.BOOKMARK_GUID_PREFIX }, 120 (bookmark) => existingBookmarksMap.set(bookmark.url.href, bookmark) 121 ); 122 123 // --------- STEP 2 --------- 124 // 125 // /=====/====\=====\ 126 // / / \ \ 127 // | | | | 128 // | A | {} | B | 129 // | | | | 130 // \ \ / / 131 // \=====\====/=====/ 132 // 133 // Find the intersection of the two lists. Items in the intersection 134 // are removed from the original lists. 135 // 136 // The items remaining in list A are new bookmarks to be added. 137 // The items remaining in list B are old bookmarks to be removed. 138 // 139 // There's nothing to do with items in the intersection, so there's no 140 // need to keep track of them. 141 // 142 // BONUS: It's necessary to keep track of the folder names that were 143 // seen, to make sure we remove the ones that were left empty. 144 145 let foldersSeen = new Set(); 146 147 for (let [url, item] of specifiedBookmarksMap) { 148 foldersSeen.add(item.Folder); 149 150 if (existingBookmarksMap.has(url)) { 151 log.debug(`Bookmark intersection: ${url}`); 152 // If this specified bookmark exists in the existing bookmarks list, 153 // we can remove it from both lists as it's in the intersection. 154 specifiedBookmarksMap.delete(url); 155 existingBookmarksMap.delete(url); 156 } 157 } 158 159 for (let url of specifiedBookmarksMap.keys()) { 160 log.debug(`Bookmark to add: ${url}`); 161 } 162 163 for (let url of existingBookmarksMap.keys()) { 164 log.debug(`Bookmark to remove: ${url}`); 165 } 166 167 // SET of folders to be deleted (bookmarks object from Places) 168 let foldersToRemove = new Set(); 169 170 // If no bookmarks will be deleted, then no folder will 171 // need to be deleted either, so this next section can be skipped. 172 if (existingBookmarksMap.size > 0) { 173 await PlacesUtils.bookmarks.fetch( 174 { guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX }, 175 (folder) => { 176 if (!foldersSeen.has(folder.title)) { 177 log.debug(`Folder to remove: ${folder.title}`); 178 foldersToRemove.add(folder); 179 } 180 } 181 ); 182 } 183 184 return { 185 add: specifiedBookmarksMap, 186 remove: existingBookmarksMap, 187 emptyFolders: foldersToRemove 188 }; 189} 190 191async function insertBookmark(bookmark) { 192 let parentGuid = await getParentGuid(bookmark.Placement, 193 bookmark.Folder); 194 195 await PlacesUtils.bookmarks.insert({ 196 url: Services.io.newURI(bookmark.URL.href), 197 title: bookmark.Title, 198 guid: generateGuidWithPrefix(BookmarksPolicies.BOOKMARK_GUID_PREFIX), 199 parentGuid, 200 }); 201 202 if (bookmark.Favicon) { 203 await setFaviconForBookmark(bookmark).catch( 204 () => log.error(`Error setting favicon for ${bookmark.Title}`)); 205 } 206} 207 208async function setFaviconForBookmark(bookmark) { 209 let faviconURI; 210 let nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); 211 212 switch (bookmark.Favicon.protocol) { 213 case "data:": 214 // data urls must first call replaceFaviconDataFromDataURL, using a 215 // fake URL. Later, it's needed to call setAndFetchFaviconForPage 216 // with the same URL. 217 faviconURI = Services.io.newURI("fake-favicon-uri:" + bookmark.URL.href); 218 219 PlacesUtils.favicons.replaceFaviconDataFromDataURL( 220 faviconURI, 221 bookmark.Favicon.href, 222 0, /* max expiration length */ 223 nullPrincipal 224 ); 225 break; 226 227 case "http:": 228 case "https:": 229 faviconURI = Services.io.newURI(bookmark.Favicon.href); 230 break; 231 232 default: 233 log.error(`Bad URL given for favicon on bookmark "${bookmark.Title}"`); 234 return Promise.resolve(); 235 } 236 237 return new Promise(resolve => { 238 PlacesUtils.favicons.setAndFetchFaviconForPage( 239 Services.io.newURI(bookmark.URL.href), 240 faviconURI, 241 false, /* forceReload */ 242 PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, 243 resolve, 244 nullPrincipal 245 ); 246 }); 247} 248 249function generateGuidWithPrefix(prefix) { 250 // Generates a random GUID and replace its beginning with the given 251 // prefix. We do this instead of just prepending the prefix to keep 252 // the correct character length. 253 return prefix + PlacesUtils.history.makeGuid().substring(prefix.length); 254} 255 256// Cache of folder names to guids to be used by the getParentGuid 257// function. The name consists in the parentGuid (which should always 258// be the menuGuid or the toolbarGuid) + the folder title. This is to 259// support having the same folder name in both the toolbar and menu. 260XPCOMUtils.defineLazyGetter(this, "gFoldersMapPromise", () => { 261 return new Promise(resolve => { 262 let foldersMap = new Map(); 263 return PlacesUtils.bookmarks.fetch( 264 { 265 guidPrefix: BookmarksPolicies.FOLDER_GUID_PREFIX 266 }, 267 (result) => { 268 foldersMap.set(`${result.parentGuid}|${result.title}`, result.guid); 269 } 270 ).then(() => resolve(foldersMap)); 271 }); 272}); 273 274async function getParentGuid(placement, folderTitle) { 275 // Defaults to toolbar if no placement was given. 276 let parentGuid = (placement == "menu") ? 277 PlacesUtils.bookmarks.menuGuid : 278 PlacesUtils.bookmarks.toolbarGuid; 279 280 if (!folderTitle) { 281 // If no folderTitle is given, this bookmark is to be placed directly 282 // into the toolbar or menu. 283 return parentGuid; 284 } 285 286 let foldersMap = await gFoldersMapPromise; 287 let folderName = `${parentGuid}|${folderTitle}`; 288 289 if (foldersMap.has(folderName)) { 290 return foldersMap.get(folderName); 291 } 292 293 let guid = generateGuidWithPrefix(BookmarksPolicies.FOLDER_GUID_PREFIX); 294 await PlacesUtils.bookmarks.insert({ 295 type: PlacesUtils.bookmarks.TYPE_FOLDER, 296 title: folderTitle, 297 guid, 298 parentGuid 299 }); 300 301 foldersMap.set(folderName, guid); 302 return guid; 303} 304