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