distribution.sys.mjs (20274B)
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 const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = 6 "distribution-customization-complete"; 7 8 const PREF_CACHED_FILE_EXISTENCE = "distribution.iniFile.exists.value"; 9 const PREF_CACHED_FILE_APPVERSION = "distribution.iniFile.exists.appversion"; 10 11 // These prefixes must only contain characters 12 // allowed by PlacesUtils.isValidGuid 13 const BOOKMARK_GUID_PREFIX = "DstB-"; 14 const FOLDER_GUID_PREFIX = "DstF-"; 15 16 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 17 18 const lazy = {}; 19 ChromeUtils.defineESModuleGetters(lazy, { 20 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 21 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 22 }); 23 24 export function DistributionCustomizer() {} 25 26 DistributionCustomizer.prototype = { 27 get _iniFile() { 28 // For parallel xpcshell testing purposes allow loading the distribution.ini 29 // file from the profile folder through an hidden pref. 30 let loadFromProfile = Services.prefs.getBoolPref( 31 "distribution.testing.loadFromProfile", 32 false 33 ); 34 35 let iniFile; 36 try { 37 iniFile = loadFromProfile 38 ? Services.dirsvc.get("ProfD", Ci.nsIFile) 39 : Services.dirsvc.get("XREAppDist", Ci.nsIFile); 40 if (loadFromProfile) { 41 iniFile.leafName = "distribution"; 42 } 43 iniFile.append("distribution.ini"); 44 } catch (ex) {} 45 46 this.__defineGetter__("_iniFile", () => iniFile); 47 return iniFile; 48 }, 49 50 get _hasDistributionIni() { 51 if (Services.prefs.prefHasUserValue(PREF_CACHED_FILE_EXISTENCE)) { 52 let knownForVersion = Services.prefs.getStringPref( 53 PREF_CACHED_FILE_APPVERSION, 54 "unknown" 55 ); 56 // StartupCacheInfo isn't available in xpcshell tests. 57 if ( 58 knownForVersion == AppConstants.MOZ_APP_VERSION && 59 (Cu.isInAutomation || 60 Cc["@mozilla.org/startupcacheinfo;1"].getService( 61 Ci.nsIStartupCacheInfo 62 ).FoundDiskCacheOnInit) 63 ) { 64 return Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE); 65 } 66 } 67 68 let fileExists = this._iniFile.exists(); 69 Services.prefs.setBoolPref(PREF_CACHED_FILE_EXISTENCE, fileExists); 70 Services.prefs.setStringPref( 71 PREF_CACHED_FILE_APPVERSION, 72 AppConstants.MOZ_APP_VERSION 73 ); 74 75 this.__defineGetter__("_hasDistributionIni", () => fileExists); 76 return fileExists; 77 }, 78 79 get _ini() { 80 let ini = null; 81 try { 82 if (this._hasDistributionIni) { 83 ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] 84 .getService(Ci.nsIINIParserFactory) 85 .createINIParser(this._iniFile); 86 } 87 } catch (e) { 88 if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { 89 // We probably had cached the file existence as true, 90 // but it no longer exists. We could set the new cache 91 // value here, but let's just invalidate the cache and 92 // let it be cached by a single code path on the next check. 93 Services.prefs.clearUserPref(PREF_CACHED_FILE_EXISTENCE); 94 } else { 95 // Unable to parse INI. 96 console.error("Unable to parse distribution.ini"); 97 } 98 } 99 this.__defineGetter__("_ini", () => ini); 100 return this._ini; 101 }, 102 103 get _locale() { 104 const locale = Services.locale.requestedLocale || "en-US"; 105 this.__defineGetter__("_locale", () => locale); 106 return this._locale; 107 }, 108 109 get _language() { 110 let language = this._locale.split("-")[0]; 111 this.__defineGetter__("_language", () => language); 112 return this._language; 113 }, 114 115 async _removeDistributionBookmarks() { 116 await lazy.PlacesUtils.bookmarks.fetch( 117 { guidPrefix: BOOKMARK_GUID_PREFIX }, 118 bookmark => 119 lazy.PlacesUtils.bookmarks.remove(bookmark).catch(console.error) 120 ); 121 await lazy.PlacesUtils.bookmarks.fetch( 122 { guidPrefix: FOLDER_GUID_PREFIX }, 123 folder => { 124 lazy.PlacesUtils.bookmarks.remove(folder).catch(console.error); 125 } 126 ); 127 }, 128 129 async _parseBookmarksSection(parentGuid, section) { 130 let keys = Array.from(this._ini.getKeys(section)).sort(); 131 let re = /^item\.(\d+)\.(\w+)\.?(\w*)/; 132 let items = {}; 133 let defaultIndex = -1; 134 let maxIndex = -1; 135 136 for (let key of keys) { 137 let m = re.exec(key); 138 if (m) { 139 let [, itemIndex, iprop, ilocale] = m; 140 itemIndex = parseInt(itemIndex); 141 142 if (ilocale) { 143 continue; 144 } 145 146 if (keys.includes(key + "." + this._locale)) { 147 key += "." + this._locale; 148 } else if (keys.includes(key + "." + this._language)) { 149 key += "." + this._language; 150 } 151 152 if (!items[itemIndex]) { 153 items[itemIndex] = {}; 154 } 155 items[itemIndex][iprop] = this._ini.getString(section, key); 156 157 if (iprop == "type" && items[itemIndex].type == "default") { 158 defaultIndex = itemIndex; 159 } 160 161 if (maxIndex < itemIndex) { 162 maxIndex = itemIndex; 163 } 164 } else { 165 dump(`Key did not match: ${key}\n`); 166 } 167 } 168 169 let prependIndex = 0; 170 for (let itemIndex = 0; itemIndex <= maxIndex; itemIndex++) { 171 if (!items[itemIndex]) { 172 continue; 173 } 174 175 let index = lazy.PlacesUtils.bookmarks.DEFAULT_INDEX; 176 let item = items[itemIndex]; 177 178 switch (item.type) { 179 case "default": 180 break; 181 182 case "folder": { 183 if (itemIndex < defaultIndex) { 184 index = prependIndex++; 185 } 186 187 let folder = await lazy.PlacesUtils.bookmarks.insert({ 188 type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, 189 guid: lazy.PlacesUtils.generateGuidWithPrefix(FOLDER_GUID_PREFIX), 190 parentGuid, 191 index, 192 title: item.title, 193 }); 194 195 await this._parseBookmarksSection( 196 folder.guid, 197 "BookmarksFolder-" + item.folderId 198 ); 199 break; 200 } 201 202 case "separator": 203 if (itemIndex < defaultIndex) { 204 index = prependIndex++; 205 } 206 207 await lazy.PlacesUtils.bookmarks.insert({ 208 type: lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR, 209 parentGuid, 210 index, 211 }); 212 break; 213 214 case "livemark": 215 // Livemarks are no more supported, instead of a livemark we'll insert 216 // a bookmark pointing to the site uri, if available. 217 if (!item.siteLink) { 218 break; 219 } 220 if (itemIndex < defaultIndex) { 221 index = prependIndex++; 222 } 223 224 await lazy.PlacesUtils.bookmarks.insert({ 225 parentGuid, 226 index, 227 title: item.title, 228 url: item.siteLink, 229 }); 230 break; 231 232 case "bookmark": 233 default: 234 if (itemIndex < defaultIndex) { 235 index = prependIndex++; 236 } 237 238 await lazy.PlacesUtils.bookmarks.insert({ 239 guid: lazy.PlacesUtils.generateGuidWithPrefix(BOOKMARK_GUID_PREFIX), 240 parentGuid, 241 index, 242 title: item.title, 243 url: item.link, 244 }); 245 246 if (item.icon && item.iconData) { 247 try { 248 lazy.PlacesUtils.favicons 249 .setFaviconForPage( 250 Services.io.newURI(item.link), 251 Services.io.newURI(item.icon), 252 Services.io.newURI(item.iconData) 253 ) 254 .catch(console.error); 255 } catch (e) { 256 console.error(e); 257 } 258 } 259 260 break; 261 } 262 } 263 }, 264 265 _newProfile: false, 266 _customizationsApplied: false, 267 applyCustomizations: function DIST_applyCustomizations() { 268 this._customizationsApplied = true; 269 270 if (!Services.prefs.prefHasUserValue("browser.migration.version")) { 271 this._newProfile = true; 272 } 273 274 if (!this._ini) { 275 return this._checkCustomizationComplete(); 276 } 277 278 if (!this._prefDefaultsApplied) { 279 this.applyPrefDefaults(); 280 } 281 }, 282 283 _bookmarksApplied: false, 284 async applyBookmarks() { 285 let prefs = Services.prefs 286 .getChildList("distribution.yandex") 287 .concat(Services.prefs.getChildList("distribution.mailru")) 288 .concat(Services.prefs.getChildList("distribution.okru")); 289 if (prefs.length) { 290 let extensionIDs = [ 291 "sovetnik-yandex@yandex.ru", 292 "vb@yandex.ru", 293 "ntp-mail@corp.mail.ru", 294 "ntp-okru@corp.mail.ru", 295 ]; 296 for (let extensionID of extensionIDs) { 297 let addon = await lazy.AddonManager.getAddonByID(extensionID); 298 if (addon) { 299 await addon.disable(); 300 } 301 } 302 for (let pref of prefs) { 303 Services.prefs.clearUserPref(pref); 304 } 305 await this._removeDistributionBookmarks(); 306 } else { 307 await this._doApplyBookmarks(); 308 } 309 this._bookmarksApplied = true; 310 this._checkCustomizationComplete(); 311 }, 312 313 async _doApplyBookmarks() { 314 if (!this._ini) { 315 return; 316 } 317 318 let sections = enumToObject(this._ini.getSections()); 319 320 // The global section, and several of its fields, is required 321 // (we also check here to be consistent with applyPrefDefaults below) 322 if (!sections.Global) { 323 return; 324 } 325 326 let globalPrefs = enumToObject(this._ini.getKeys("Global")); 327 if (!(globalPrefs.id && globalPrefs.version && globalPrefs.about)) { 328 return; 329 } 330 331 let bmProcessedPref; 332 try { 333 bmProcessedPref = this._ini.getString( 334 "Global", 335 "bookmarks.initialized.pref" 336 ); 337 } catch (e) { 338 bmProcessedPref = 339 "distribution." + 340 this._ini.getString("Global", "id") + 341 ".bookmarksProcessed"; 342 } 343 344 if (Services.prefs.getBoolPref(bmProcessedPref, false)) { 345 return; 346 } 347 348 let { ProfileAge } = ChromeUtils.importESModule( 349 "resource://gre/modules/ProfileAge.sys.mjs" 350 ); 351 let profileAge = await ProfileAge(); 352 let resetDate = await profileAge.reset; 353 354 // If the profile has been reset, don't recreate bookmarks. 355 if (!resetDate) { 356 if (sections.BookmarksMenu) { 357 await this._parseBookmarksSection( 358 lazy.PlacesUtils.bookmarks.menuGuid, 359 "BookmarksMenu" 360 ); 361 } 362 if (sections.BookmarksToolbar) { 363 await this._parseBookmarksSection( 364 lazy.PlacesUtils.bookmarks.toolbarGuid, 365 "BookmarksToolbar" 366 ); 367 } 368 } 369 Services.prefs.setBoolPref(bmProcessedPref, true); 370 }, 371 372 _prefDefaultsApplied: false, 373 applyPrefDefaults: function DIST_applyPrefDefaults() { 374 this._prefDefaultsApplied = true; 375 if (!this._ini) { 376 return this._checkCustomizationComplete(); 377 } 378 379 let sections = enumToObject(this._ini.getSections()); 380 381 // The global section, and several of its fields, is required 382 if (!sections.Global) { 383 return this._checkCustomizationComplete(); 384 } 385 let globalPrefs = enumToObject(this._ini.getKeys("Global")); 386 if (!(globalPrefs.id && globalPrefs.version)) { 387 return this._checkCustomizationComplete(); 388 } 389 let distroID = this._ini.getString("Global", "id"); 390 if (!globalPrefs.about && !distroID.startsWith("mozilla-")) { 391 // About is required unless it is a mozilla distro. 392 return this._checkCustomizationComplete(); 393 } 394 395 let defaults = Services.prefs.getDefaultBranch(null); 396 397 // Global really contains info we set as prefs. They're only 398 // separate because they are "special" (read: required) 399 400 defaults.setStringPref("distribution.id", distroID); 401 402 if ( 403 distroID.startsWith("yandex") || 404 distroID.startsWith("mailru") || 405 distroID.startsWith("okru") 406 ) { 407 this.__defineGetter__("_ini", () => null); 408 return this._checkCustomizationComplete(); 409 } 410 411 defaults.setStringPref( 412 "distribution.version", 413 this._ini.getString("Global", "version") 414 ); 415 416 let partnerAbout; 417 try { 418 if (globalPrefs["about." + this._locale]) { 419 partnerAbout = this._ini.getString("Global", "about." + this._locale); 420 } else if (globalPrefs["about." + this._language]) { 421 partnerAbout = this._ini.getString("Global", "about." + this._language); 422 } else { 423 partnerAbout = this._ini.getString("Global", "about"); 424 } 425 defaults.setStringPref("distribution.about", partnerAbout); 426 } catch (e) { 427 /* ignore bad prefs due to bug 895473 and move on */ 428 } 429 430 /* order of precedence is locale->language->default */ 431 432 let preferences = new Map(); 433 434 if (sections.Preferences) { 435 for (let key of this._ini.getKeys("Preferences")) { 436 let value = this._ini.getString("Preferences", key); 437 if (value) { 438 preferences.set(key, value); 439 } 440 } 441 } 442 443 if (sections["Preferences-" + this._language]) { 444 for (let key of this._ini.getKeys("Preferences-" + this._language)) { 445 let value = this._ini.getString("Preferences-" + this._language, key); 446 if (value) { 447 preferences.set(key, value); 448 } else { 449 // If something was set by Preferences, but it's empty in language, 450 // it should be removed. 451 preferences.delete(key); 452 } 453 } 454 } 455 456 if (sections["Preferences-" + this._locale]) { 457 for (let key of this._ini.getKeys("Preferences-" + this._locale)) { 458 let value = this._ini.getString("Preferences-" + this._locale, key); 459 if (value) { 460 preferences.set(key, value); 461 } else { 462 // If something was set by Preferences, but it's empty in locale, 463 // it should be removed. 464 preferences.delete(key); 465 } 466 } 467 } 468 469 for (let [prefName, prefValue] of preferences) { 470 prefValue = prefValue.replace(/%LOCALE%/g, this._locale); 471 prefValue = prefValue.replace(/%LANGUAGE%/g, this._language); 472 prefValue = parseValue(prefValue); 473 try { 474 if (prefName == "general.useragent.locale") { 475 defaults.setStringPref("intl.locale.requested", prefValue); 476 } else { 477 switch (typeof prefValue) { 478 case "boolean": 479 defaults.setBoolPref(prefName, prefValue); 480 break; 481 case "number": 482 defaults.setIntPref(prefName, prefValue); 483 break; 484 case "string": 485 defaults.setStringPref(prefName, prefValue); 486 break; 487 } 488 } 489 } catch (e) { 490 /* ignore bad prefs and move on */ 491 } 492 } 493 494 if (this._ini.getString("Global", "id") == "yandex") { 495 // All yandex distributions have the same distribution ID, 496 // so we're using an internal preference to name them correctly. 497 // This is needed for search to work properly. 498 try { 499 defaults.setStringPref( 500 "distribution.id", 501 defaults 502 .get("extensions.yasearch@yandex.ru.clids.vendor") 503 .replace("firefox", "yandex") 504 ); 505 } catch (e) { 506 // Just use the default distribution ID. 507 } 508 } 509 510 let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( 511 Ci.nsIPrefLocalizedString 512 ); 513 514 let localizablePreferences = new Map(); 515 516 if (sections.LocalizablePreferences) { 517 for (let key of this._ini.getKeys("LocalizablePreferences")) { 518 let value = this._ini.getString("LocalizablePreferences", key); 519 if (value) { 520 localizablePreferences.set(key, value); 521 } 522 } 523 } 524 525 if (sections["LocalizablePreferences-" + this._language]) { 526 for (let key of this._ini.getKeys( 527 "LocalizablePreferences-" + this._language 528 )) { 529 let value = this._ini.getString( 530 "LocalizablePreferences-" + this._language, 531 key 532 ); 533 if (value) { 534 localizablePreferences.set(key, value); 535 } else { 536 // If something was set by Preferences, but it's empty in language, 537 // it should be removed. 538 localizablePreferences.delete(key); 539 } 540 } 541 } 542 543 if (sections["LocalizablePreferences-" + this._locale]) { 544 for (let key of this._ini.getKeys( 545 "LocalizablePreferences-" + this._locale 546 )) { 547 let value = this._ini.getString( 548 "LocalizablePreferences-" + this._locale, 549 key 550 ); 551 if (value) { 552 localizablePreferences.set(key, value); 553 } else { 554 // If something was set by Preferences, but it's empty in locale, 555 // it should be removed. 556 localizablePreferences.delete(key); 557 } 558 } 559 } 560 561 for (let [prefName, prefValue] of localizablePreferences) { 562 prefValue = parseValue(prefValue); 563 prefValue = prefValue.replace(/%LOCALE%/g, this._locale); 564 prefValue = prefValue.replace(/%LANGUAGE%/g, this._language); 565 localizedStr.data = "data:text/plain," + prefName + "=" + prefValue; 566 try { 567 defaults.setComplexValue( 568 prefName, 569 Ci.nsIPrefLocalizedString, 570 localizedStr 571 ); 572 } catch (e) { 573 /* ignore bad prefs and move on */ 574 } 575 } 576 577 return this._checkCustomizationComplete(); 578 }, 579 580 _checkCustomizationComplete: function DIST__checkCustomizationComplete() { 581 const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; 582 583 if (this._newProfile) { 584 try { 585 var showPersonalToolbar = Services.prefs.getBoolPref( 586 "browser.showPersonalToolbar" 587 ); 588 if (showPersonalToolbar) { 589 Services.prefs.setCharPref( 590 "browser.toolbars.bookmarks.visibility", 591 "always" 592 ); 593 } 594 } catch (e) {} 595 try { 596 var showMenubar = Services.prefs.getBoolPref("browser.showMenubar"); 597 if (showMenubar) { 598 Services.xulStore.setValue( 599 BROWSER_DOCURL, 600 "toolbar-menubar", 601 "autohide", 602 "false" 603 ); 604 } 605 } catch (e) {} 606 // If a theme was specified in the distribution, and it's a new profile, 607 // set the theme as default. 608 try { 609 const activeThemeID = Services.prefs.getCharPref( 610 "extensions.activeThemeID" 611 ); 612 if (activeThemeID) { 613 lazy.AddonManager.getAddonByID(activeThemeID).then(addon => 614 addon?.enable() 615 ); 616 } 617 } catch (e) {} 618 } 619 620 let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini; 621 if ( 622 this._customizationsApplied && 623 this._bookmarksApplied && 624 prefDefaultsApplied 625 ) { 626 Services.obs.notifyObservers( 627 null, 628 DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC 629 ); 630 } 631 }, 632 }; 633 634 function parseValue(value) { 635 try { 636 value = JSON.parse(value); 637 } catch (e) { 638 // JSON.parse catches numbers and booleans. 639 // Anything else, we assume is a string. 640 // Remove the quotes that aren't needed anymore. 641 value = value.replace(/^"/, ""); 642 value = value.replace(/"$/, ""); 643 } 644 return value; 645 } 646 647 function enumToObject(UTF8Enumerator) { 648 let ret = {}; 649 for (let i of UTF8Enumerator) { 650 ret[i] = 1; 651 } 652 return ret; 653 } 654 655 export let DistributionManagement = { 656 _distributionCustomizer: null, 657 get BOOKMARK_GUID_PREFIX() { 658 return BOOKMARK_GUID_PREFIX; 659 }, 660 get FOLDER_GUID_PREFIX() { 661 return FOLDER_GUID_PREFIX; 662 }, 663 664 _ensureCustomizer() { 665 if (this._distributionCustomizer) { 666 return; 667 } 668 this._distributionCustomizer = new DistributionCustomizer(); 669 Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC); 670 }, 671 672 observe(_subject, topic) { 673 if (topic == DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC) { 674 Services.obs.removeObserver( 675 this, 676 DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC 677 ); 678 this._distributionCustomizer = null; 679 } 680 }, 681 682 applyCustomizations() { 683 this._ensureCustomizer(); 684 this._distributionCustomizer.applyCustomizations(); 685 }, 686 687 applyBookmarks() { 688 this._ensureCustomizer(); 689 this._distributionCustomizer.applyBookmarks(); 690 }, 691 692 QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]), 693 };