addonutils.sys.mjs (13092B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { Log } from "resource://gre/modules/Log.sys.mjs"; 6 7 import { Svc } from "resource://services-sync/util.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 13 AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", 14 }); 15 16 function AddonUtilsInternal() { 17 this._log = Log.repository.getLogger("Sync.AddonUtils"); 18 this._log.Level = 19 Log.Level[Svc.PrefBranch.getStringPref("log.logger.addonutils", null)]; 20 } 21 AddonUtilsInternal.prototype = { 22 /** 23 * Obtain an AddonInstall object from an AddonSearchResult instance. 24 * 25 * The returned promise will be an AddonInstall on success or null (failure or 26 * addon not found) 27 * 28 * @param addon 29 * AddonSearchResult to obtain install from. 30 */ 31 getInstallFromSearchResult(addon) { 32 this._log.debug("Obtaining install for " + addon.id); 33 34 // We should theoretically be able to obtain (and use) addon.install if 35 // it is available. However, the addon.sourceURI rewriting won't be 36 // reflected in the AddonInstall, so we can't use it. If we ever get rid 37 // of sourceURI rewriting, we can avoid having to reconstruct the 38 // AddonInstall. 39 return lazy.AddonManager.getInstallForURL(addon.sourceURI.spec, { 40 name: addon.name, 41 icons: addon.iconURL, 42 version: addon.version, 43 telemetryInfo: { source: "sync" }, 44 }); 45 }, 46 47 /** 48 * Installs an add-on from an AddonSearchResult instance. 49 * 50 * The options argument defines extra options to control the install. 51 * Recognized keys in this map are: 52 * 53 * syncGUID - Sync GUID to use for the new add-on. 54 * enabled - Boolean indicating whether the add-on should be enabled upon 55 * install. 56 * 57 * The result object has the following keys: 58 * 59 * id ID of add-on that was installed. 60 * install AddonInstall that was installed. 61 * addon Addon that was installed. 62 * 63 * @param addon 64 * AddonSearchResult to install add-on from. 65 * @param options 66 * Object with additional metadata describing how to install add-on. 67 */ 68 async installAddonFromSearchResult(addon, options) { 69 this._log.info("Trying to install add-on from search result: " + addon.id); 70 71 const install = await this.getInstallFromSearchResult(addon); 72 if (!install) { 73 throw new Error("AddonInstall not available: " + addon.id); 74 } 75 76 try { 77 this._log.info("Installing " + addon.id); 78 let log = this._log; 79 80 return new Promise((res, rej) => { 81 let listener = { 82 onInstallStarted: function onInstallStarted(install) { 83 if (!options) { 84 return; 85 } 86 87 if (options.syncGUID) { 88 log.info( 89 "Setting syncGUID of " + install.name + ": " + options.syncGUID 90 ); 91 install.addon.syncGUID = options.syncGUID; 92 } 93 94 // We only need to change userDisabled if it is disabled because 95 // enabled is the default. 96 if ("enabled" in options && !options.enabled) { 97 log.info( 98 "Marking add-on as disabled for install: " + install.name 99 ); 100 install.addon.disable(); 101 } 102 }, 103 onInstallEnded(install, addon) { 104 // Themes-addons are not automatically enabled when installed, but the active 105 // theme is synced by prefs, so we use a transient pref to enable it 106 // after install 107 let hasIncomingActiveThemeId = Services.prefs.getStringPref( 108 "extensions.pendingActiveThemeID", 109 "" 110 ); 111 if ( 112 hasIncomingActiveThemeId && 113 addon.id === hasIncomingActiveThemeId 114 ) { 115 try { 116 addon.enable(); 117 } catch (e) { 118 this._log.error("Failed to enable the incoming theme", e); 119 } finally { 120 // If something went wrong with enabling the theme, we don't have a good 121 // way to retry -- so we'll clear it rather than keeping the pref around 122 Services.prefs.clearUserPref("extensions.pendingActiveThemeID"); 123 } 124 } 125 install.removeListener(listener); 126 127 res({ id: addon.id, install, addon }); 128 }, 129 onInstallFailed(install) { 130 install.removeListener(listener); 131 132 rej(new Error("Install failed: " + install.error)); 133 }, 134 onDownloadFailed(install) { 135 install.removeListener(listener); 136 137 rej(new Error("Download failed: " + install.error)); 138 }, 139 }; 140 install.addListener(listener); 141 install.install(); 142 }); 143 } catch (ex) { 144 this._log.error("Error installing add-on", ex); 145 throw ex; 146 } 147 }, 148 149 /** 150 * Uninstalls the addon instance. 151 * 152 * @param addon 153 * Addon instance to uninstall. 154 */ 155 async uninstallAddon(addon) { 156 return new Promise(res => { 157 let listener = { 158 onUninstalling(uninstalling, needsRestart) { 159 if (addon.id != uninstalling.id) { 160 return; 161 } 162 163 // We assume restartless add-ons will send the onUninstalled event 164 // soon. 165 if (!needsRestart) { 166 return; 167 } 168 169 // For non-restartless add-ons, we issue the callback on uninstalling 170 // because we will likely never see the uninstalled event. 171 lazy.AddonManager.removeAddonListener(listener); 172 res(addon); 173 }, 174 onUninstalled(uninstalled) { 175 if (addon.id != uninstalled.id) { 176 return; 177 } 178 179 lazy.AddonManager.removeAddonListener(listener); 180 res(addon); 181 }, 182 }; 183 lazy.AddonManager.addAddonListener(listener); 184 addon.uninstall(); 185 }); 186 }, 187 188 /** 189 * Installs multiple add-ons specified by metadata. 190 * 191 * The first argument is an array of objects. Each object must have the 192 * following keys: 193 * 194 * id - public ID of the add-on to install. 195 * syncGUID - syncGUID for new add-on. 196 * enabled - boolean indicating whether the add-on should be enabled. 197 * requireSecureURI - Boolean indicating whether to require a secure 198 * URI when installing from a remote location. This defaults to 199 * true. 200 * 201 * The callback will be called when activity on all add-ons is complete. The 202 * callback receives 2 arguments, error and result. 203 * 204 * If error is truthy, it contains a string describing the overall error. 205 * 206 * The 2nd argument to the callback is always an object with details on the 207 * overall execution state. It contains the following keys: 208 * 209 * installedIDs Array of add-on IDs that were installed. 210 * installs Array of AddonInstall instances that were installed. 211 * addons Array of Addon instances that were installed. 212 * errors Array of errors encountered. Only has elements if error is 213 * truthy. 214 * 215 * @param installs 216 * Array of objects describing add-ons to install. 217 */ 218 async installAddons(installs) { 219 let ids = []; 220 for (let addon of installs) { 221 ids.push(addon.id); 222 } 223 224 let addons = await lazy.AddonRepository.getAddonsByIDs(ids); 225 this._log.info( 226 `Found ${addons.length} / ${ids.length}` + 227 " add-ons during repository search." 228 ); 229 230 let ourResult = { 231 installedIDs: [], 232 installs: [], 233 addons: [], 234 skipped: [], 235 errors: [], 236 }; 237 238 let toInstall = []; 239 240 // Rewrite the "src" query string parameter of the source URI to note 241 // that the add-on was installed by Sync and not something else so 242 // server-side metrics aren't skewed (bug 708134). The server should 243 // ideally send proper URLs, but this solution was deemed too 244 // complicated at the time the functionality was implemented. 245 for (let addon of addons) { 246 // Find the specified options for this addon. 247 let options; 248 for (let install of installs) { 249 if (install.id == addon.id) { 250 options = install; 251 break; 252 } 253 } 254 if (!this.canInstallAddon(addon, options)) { 255 ourResult.skipped.push(addon.id); 256 continue; 257 } 258 259 // We can go ahead and attempt to install it. 260 toInstall.push(addon); 261 262 // We should always be able to QI the nsIURI to nsIURL. If not, we 263 // still try to install the add-on, but we don't rewrite the URL, 264 // potentially skewing metrics. 265 try { 266 addon.sourceURI.QueryInterface(Ci.nsIURL); 267 } catch (ex) { 268 this._log.warn( 269 "Unable to QI sourceURI to nsIURL: " + addon.sourceURI.spec 270 ); 271 continue; 272 } 273 274 let params = addon.sourceURI.query 275 .split("&") 276 .map(function rewrite(param) { 277 if (param.indexOf("src=") == 0) { 278 return "src=sync"; 279 } 280 return param; 281 }); 282 283 addon.sourceURI = addon.sourceURI 284 .mutate() 285 .setQuery(params.join("&")) 286 .finalize(); 287 } 288 289 if (!toInstall.length) { 290 return ourResult; 291 } 292 293 const installPromises = []; 294 // Start all the installs asynchronously. They will report back to us 295 // as they finish, eventually triggering the global callback. 296 for (let addon of toInstall) { 297 let options = {}; 298 for (let install of installs) { 299 if (install.id == addon.id) { 300 options = install; 301 break; 302 } 303 } 304 305 installPromises.push( 306 (async () => { 307 try { 308 const result = await this.installAddonFromSearchResult( 309 addon, 310 options 311 ); 312 ourResult.installedIDs.push(result.id); 313 ourResult.installs.push(result.install); 314 ourResult.addons.push(result.addon); 315 } catch (error) { 316 ourResult.errors.push(error); 317 } 318 })() 319 ); 320 } 321 322 await Promise.all(installPromises); 323 324 if (ourResult.errors.length) { 325 throw new Error("1 or more add-ons failed to install"); 326 } 327 return ourResult; 328 }, 329 330 /** 331 * Returns true if we are able to install the specified addon, false 332 * otherwise. It is expected that this will log the reason if it returns 333 * false. 334 * 335 * @param addon 336 * (Addon) Add-on instance to check. 337 * @param options 338 * (object) The options specified for this addon. See installAddons() 339 * for the valid elements. 340 */ 341 canInstallAddon(addon, options) { 342 // sourceURI presence isn't enforced by AddonRepository. So, we skip 343 // add-ons without a sourceURI. 344 if (!addon.sourceURI) { 345 this._log.info( 346 "Skipping install of add-on because missing sourceURI: " + addon.id 347 ); 348 return false; 349 } 350 // Verify that the source URI uses TLS. We don't allow installs from 351 // insecure sources for security reasons. The Addon Manager ensures 352 // that cert validation etc is performed. 353 // (We should also consider just dropping this entirely and calling 354 // XPIProvider.isInstallAllowed, but that has additional semantics we might 355 // need to think through...) 356 let requireSecureURI = true; 357 if (options && options.requireSecureURI !== undefined) { 358 requireSecureURI = options.requireSecureURI; 359 } 360 361 if (requireSecureURI) { 362 let scheme = addon.sourceURI.scheme; 363 if (scheme != "https") { 364 this._log.info( 365 `Skipping install of add-on "${addon.id}" because sourceURI's scheme of "${scheme}" is not trusted` 366 ); 367 return false; 368 } 369 } 370 371 // Policy prevents either installing this addon or any addon 372 if ( 373 Services.policies && 374 (!Services.policies.mayInstallAddon(addon) || 375 !Services.policies.isAllowed("xpinstall")) 376 ) { 377 this._log.info( 378 `Skipping install of "${addon.id}" due to enterprise policy` 379 ); 380 return false; 381 } 382 383 this._log.info(`Add-on "${addon.id}" is able to be installed`); 384 return true; 385 }, 386 387 /** 388 * Update the user disabled flag for an add-on. 389 * 390 * If the new flag matches the existing or if the add-on 391 * isn't currently active, the function will return immediately. 392 * 393 * @param addon 394 * (Addon) Add-on instance to operate on. 395 * @param value 396 * (bool) New value for add-on's userDisabled property. 397 */ 398 updateUserDisabled(addon, value) { 399 if (addon.userDisabled == value) { 400 return; 401 } 402 403 this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value); 404 if (value) { 405 addon.disable(); 406 } else { 407 addon.enable(); 408 } 409 }, 410 }; 411 412 export const AddonUtils = new AddonUtilsInternal();