firefox.ts (15315B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import fs from 'node:fs'; 8 import path from 'node:path'; 9 10 import {getJSON} from '../httpUtil.js'; 11 12 import {BrowserPlatform, type ProfileOptions} from './types.js'; 13 14 function getFormat(buildId: string): string { 15 const majorVersion = Number(buildId.split('.').shift()!); 16 return majorVersion >= 135 ? 'xz' : 'bz2'; 17 } 18 19 function archiveNightly(platform: BrowserPlatform, buildId: string): string { 20 switch (platform) { 21 case BrowserPlatform.LINUX: 22 return `firefox-${buildId}.en-US.linux-x86_64.tar.${getFormat(buildId)}`; 23 case BrowserPlatform.LINUX_ARM: 24 return `firefox-${buildId}.en-US.linux-aarch64.tar.${getFormat(buildId)}`; 25 case BrowserPlatform.MAC_ARM: 26 case BrowserPlatform.MAC: 27 return `firefox-${buildId}.en-US.mac.dmg`; 28 case BrowserPlatform.WIN32: 29 case BrowserPlatform.WIN64: 30 return `firefox-${buildId}.en-US.${platform}.zip`; 31 } 32 } 33 34 function archive(platform: BrowserPlatform, buildId: string): string { 35 switch (platform) { 36 case BrowserPlatform.LINUX_ARM: 37 case BrowserPlatform.LINUX: 38 return `firefox-${buildId}.tar.${getFormat(buildId)}`; 39 case BrowserPlatform.MAC_ARM: 40 case BrowserPlatform.MAC: 41 return `Firefox ${buildId}.dmg`; 42 case BrowserPlatform.WIN32: 43 case BrowserPlatform.WIN64: 44 return `Firefox Setup ${buildId}.exe`; 45 } 46 } 47 48 function platformName(platform: BrowserPlatform): string { 49 switch (platform) { 50 case BrowserPlatform.LINUX: 51 return `linux-x86_64`; 52 case BrowserPlatform.LINUX_ARM: 53 return `linux-aarch64`; 54 case BrowserPlatform.MAC_ARM: 55 case BrowserPlatform.MAC: 56 return `mac`; 57 case BrowserPlatform.WIN32: 58 case BrowserPlatform.WIN64: 59 return platform; 60 } 61 } 62 63 function parseBuildId(buildId: string): [FirefoxChannel, string] { 64 for (const value of Object.values(FirefoxChannel)) { 65 if (buildId.startsWith(value + '_')) { 66 buildId = buildId.substring(value.length + 1); 67 return [value, buildId]; 68 } 69 } 70 // Older versions do not have channel as the prefix.« 71 return [FirefoxChannel.NIGHTLY, buildId]; 72 } 73 74 export function resolveDownloadUrl( 75 platform: BrowserPlatform, 76 buildId: string, 77 baseUrl?: string, 78 ): string { 79 const [channel] = parseBuildId(buildId); 80 switch (channel) { 81 case FirefoxChannel.NIGHTLY: 82 baseUrl ??= 83 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'; 84 break; 85 case FirefoxChannel.DEVEDITION: 86 baseUrl ??= 'https://archive.mozilla.org/pub/devedition/releases'; 87 break; 88 case FirefoxChannel.BETA: 89 case FirefoxChannel.STABLE: 90 case FirefoxChannel.ESR: 91 baseUrl ??= 'https://archive.mozilla.org/pub/firefox/releases'; 92 break; 93 } 94 return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; 95 } 96 97 export function resolveDownloadPath( 98 platform: BrowserPlatform, 99 buildId: string, 100 ): string[] { 101 const [channel, resolvedBuildId] = parseBuildId(buildId); 102 switch (channel) { 103 case FirefoxChannel.NIGHTLY: 104 return [archiveNightly(platform, resolvedBuildId)]; 105 case FirefoxChannel.DEVEDITION: 106 case FirefoxChannel.BETA: 107 case FirefoxChannel.STABLE: 108 case FirefoxChannel.ESR: 109 return [ 110 resolvedBuildId, 111 platformName(platform), 112 'en-US', 113 archive(platform, resolvedBuildId), 114 ]; 115 } 116 } 117 118 export function relativeExecutablePath( 119 platform: BrowserPlatform, 120 buildId: string, 121 ): string { 122 const [channel] = parseBuildId(buildId); 123 switch (channel) { 124 case FirefoxChannel.NIGHTLY: 125 switch (platform) { 126 case BrowserPlatform.MAC_ARM: 127 case BrowserPlatform.MAC: 128 return path.join( 129 'Firefox Nightly.app', 130 'Contents', 131 'MacOS', 132 'firefox', 133 ); 134 case BrowserPlatform.LINUX_ARM: 135 case BrowserPlatform.LINUX: 136 return path.join('firefox', 'firefox'); 137 case BrowserPlatform.WIN32: 138 case BrowserPlatform.WIN64: 139 return path.join('firefox', 'firefox.exe'); 140 } 141 case FirefoxChannel.BETA: 142 case FirefoxChannel.DEVEDITION: 143 case FirefoxChannel.ESR: 144 case FirefoxChannel.STABLE: 145 switch (platform) { 146 case BrowserPlatform.MAC_ARM: 147 case BrowserPlatform.MAC: 148 return path.join('Firefox.app', 'Contents', 'MacOS', 'firefox'); 149 case BrowserPlatform.LINUX_ARM: 150 case BrowserPlatform.LINUX: 151 return path.join('firefox', 'firefox'); 152 case BrowserPlatform.WIN32: 153 case BrowserPlatform.WIN64: 154 return path.join('core', 'firefox.exe'); 155 } 156 } 157 } 158 159 export enum FirefoxChannel { 160 STABLE = 'stable', 161 ESR = 'esr', 162 DEVEDITION = 'devedition', 163 BETA = 'beta', 164 NIGHTLY = 'nightly', 165 } 166 167 export async function resolveBuildId( 168 channel: FirefoxChannel = FirefoxChannel.NIGHTLY, 169 ): Promise<string> { 170 const channelToVersionKey = { 171 [FirefoxChannel.ESR]: 'FIREFOX_ESR', 172 [FirefoxChannel.STABLE]: 'LATEST_FIREFOX_VERSION', 173 [FirefoxChannel.DEVEDITION]: 'FIREFOX_DEVEDITION', 174 [FirefoxChannel.BETA]: 'FIREFOX_DEVEDITION', 175 [FirefoxChannel.NIGHTLY]: 'FIREFOX_NIGHTLY', 176 }; 177 const versions = (await getJSON( 178 new URL('https://product-details.mozilla.org/1.0/firefox_versions.json'), 179 )) as Record<string, string>; 180 const version = versions[channelToVersionKey[channel]]; 181 if (!version) { 182 throw new Error(`Channel ${channel} is not found.`); 183 } 184 return channel + '_' + version; 185 } 186 187 export async function createProfile(options: ProfileOptions): Promise<void> { 188 if (!fs.existsSync(options.path)) { 189 await fs.promises.mkdir(options.path, { 190 recursive: true, 191 }); 192 } 193 await syncPreferences({ 194 preferences: { 195 ...defaultProfilePreferences(options.preferences), 196 ...options.preferences, 197 }, 198 path: options.path, 199 }); 200 } 201 202 function defaultProfilePreferences( 203 extraPrefs: Record<string, unknown>, 204 ): Record<string, unknown> { 205 const server = 'dummy.test'; 206 207 const defaultPrefs = { 208 // Make sure Shield doesn't hit the network. 209 'app.normandy.api_url': '', 210 // Disable Firefox old build background check 211 'app.update.checkInstallTime': false, 212 // Disable automatically upgrading Firefox 213 'app.update.disabledForTesting': true, 214 215 // Increase the APZ content response timeout to 1 minute 216 'apz.content_response_timeout': 60000, 217 218 // Prevent various error message on the console 219 // jest-puppeteer asserts that no error message is emitted by the console 220 'browser.contentblocking.features.standard': 221 '-tp,tpPrivate,cookieBehavior0,-cryptoTP,-fp', 222 223 // Enable the dump function: which sends messages to the system 224 // console 225 // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115 226 'browser.dom.window.dump.enabled': true, 227 // Disable topstories 228 'browser.newtabpage.activity-stream.feeds.system.topstories': false, 229 // Always display a blank page 230 'browser.newtabpage.enabled': false, 231 // Background thumbnails in particular cause grief: and disabling 232 // thumbnails in general cannot hurt 233 'browser.pagethumbnails.capturing_disabled': true, 234 235 // Disable safebrowsing components. 236 'browser.safebrowsing.blockedURIs.enabled': false, 237 'browser.safebrowsing.downloads.enabled': false, 238 'browser.safebrowsing.malware.enabled': false, 239 'browser.safebrowsing.phishing.enabled': false, 240 241 // Disable updates to search engines. 242 'browser.search.update': false, 243 // Do not restore the last open set of tabs if the browser has crashed 244 'browser.sessionstore.resume_from_crash': false, 245 // Skip check for default browser on startup 246 'browser.shell.checkDefaultBrowser': false, 247 248 // Disable newtabpage 249 'browser.startup.homepage': 'about:blank', 250 // Do not redirect user when a milstone upgrade of Firefox is detected 251 'browser.startup.homepage_override.mstone': 'ignore', 252 // Start with a blank page about:blank 253 'browser.startup.page': 0, 254 255 // Do not allow background tabs to be zombified on Android: otherwise for 256 // tests that open additional tabs: the test harness tab itself might get 257 // unloaded 258 'browser.tabs.disableBackgroundZombification': false, 259 // Do not warn when closing all other open tabs 260 'browser.tabs.warnOnCloseOtherTabs': false, 261 // Do not warn when multiple tabs will be opened 262 'browser.tabs.warnOnOpen': false, 263 264 // Do not automatically offer translations, as tests do not expect this. 265 'browser.translations.automaticallyPopup': false, 266 267 // Disable the UI tour. 268 'browser.uitour.enabled': false, 269 // Turn off search suggestions in the location bar so as not to trigger 270 // network connections. 271 'browser.urlbar.suggest.searches': false, 272 // Disable first run splash page on Windows 10 273 'browser.usedOnWindows10.introURL': '', 274 // Do not warn on quitting Firefox 275 'browser.warnOnQuit': false, 276 277 // Defensively disable data reporting systems 278 'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`, 279 'datareporting.healthreport.logging.consoleEnabled': false, 280 'datareporting.healthreport.service.enabled': false, 281 'datareporting.healthreport.service.firstRun': false, 282 'datareporting.healthreport.uploadEnabled': false, 283 284 // Do not show datareporting policy notifications which can interfere with tests 285 'datareporting.policy.dataSubmissionEnabled': false, 286 'datareporting.policy.dataSubmissionPolicyBypassNotification': true, 287 288 // DevTools JSONViewer sometimes fails to load dependencies with its require.js. 289 // This doesn't affect Puppeteer but spams console (Bug 1424372) 290 'devtools.jsonview.enabled': false, 291 292 // Disable popup-blocker 293 'dom.disable_open_during_load': false, 294 295 // Enable the support for File object creation in the content process 296 // Required for |Page.setFileInputFiles| protocol method. 297 'dom.file.createInChild': true, 298 299 // Disable the ProcessHangMonitor 300 'dom.ipc.reportProcessHangs': false, 301 302 // Disable slow script dialogues 303 'dom.max_chrome_script_run_time': 0, 304 'dom.max_script_run_time': 0, 305 306 // Only load extensions from the application and user profile 307 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION 308 'extensions.autoDisableScopes': 0, 309 'extensions.enabledScopes': 5, 310 311 // Disable metadata caching for installed add-ons by default 312 'extensions.getAddons.cache.enabled': false, 313 314 // Disable installing any distribution extensions or add-ons. 315 'extensions.installDistroAddons': false, 316 317 // Turn off extension updates so they do not bother tests 318 'extensions.update.enabled': false, 319 320 // Turn off extension updates so they do not bother tests 321 'extensions.update.notifyUser': false, 322 323 // Make sure opening about:addons will not hit the network 324 'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`, 325 326 // Allow the application to have focus even it runs in the background 327 'focusmanager.testmode': true, 328 329 // Disable useragent updates 330 'general.useragent.updates.enabled': false, 331 332 // Always use network provider for geolocation tests so we bypass the 333 // macOS dialog raised by the corelocation provider 334 'geo.provider.testing': true, 335 336 // Do not scan Wifi 337 'geo.wifi.scan': false, 338 339 // No hang monitor 340 'hangmonitor.timeout': 0, 341 342 // Show chrome errors and warnings in the error console 343 'javascript.options.showInConsole': true, 344 345 // Disable download and usage of OpenH264: and Widevine plugins 346 'media.gmp-manager.updateEnabled': false, 347 348 // Disable the GFX sanity window 349 'media.sanity-test.disabled': true, 350 351 // Disable experimental feature that is only available in Nightly 352 'network.cookie.sameSite.laxByDefault': false, 353 354 // Do not prompt for temporary redirects 355 'network.http.prompt-temp-redirect': false, 356 357 // Disable speculative connections so they are not reported as leaking 358 // when they are hanging around 359 'network.http.speculative-parallel-limit': 0, 360 361 // Do not automatically switch between offline and online 362 'network.manage-offline-status': false, 363 364 // Make sure SNTP requests do not hit the network 365 'network.sntp.pools': server, 366 367 // Disable Flash. 368 'plugin.state.flash': 0, 369 370 'privacy.trackingprotection.enabled': false, 371 372 // Can be removed once Firefox 89 is no longer supported 373 // https://bugzilla.mozilla.org/show_bug.cgi?id=1710839 374 'remote.enabled': true, 375 376 // Disabled screenshots component 377 'screenshots.browser.component.enabled': false, 378 379 // Don't do network connections for mitm priming 380 'security.certerrors.mitm.priming.enabled': false, 381 382 // Local documents have access to all other local documents, 383 // including directory listings 384 'security.fileuri.strict_origin_policy': false, 385 386 // Do not wait for the notification button security delay 387 'security.notification_enable_delay': 0, 388 389 // Ensure blocklist updates do not hit the network 390 'services.settings.server': `http://${server}/dummy/blocklist/`, 391 392 // Do not automatically fill sign-in forms with known usernames and 393 // passwords 394 'signon.autofillForms': false, 395 396 // Disable password capture, so that tests that include forms are not 397 // influenced by the presence of the persistent doorhanger notification 398 'signon.rememberSignons': false, 399 400 // Disable first-run welcome page 401 'startup.homepage_welcome_url': 'about:blank', 402 403 // Disable first-run welcome page 404 'startup.homepage_welcome_url.additional': '', 405 406 // Disable browser animations (tabs, fullscreen, sliding alerts) 407 'toolkit.cosmeticAnimations.enabled': false, 408 409 // Prevent starting into safe mode after application crashes 410 'toolkit.startup.max_resumed_crashes': -1, 411 }; 412 413 return Object.assign(defaultPrefs, extraPrefs); 414 } 415 416 async function backupFile(input: string): Promise<void> { 417 if (!fs.existsSync(input)) { 418 return; 419 } 420 await fs.promises.copyFile(input, input + '.puppeteer'); 421 } 422 423 /** 424 * Populates the user.js file with custom preferences as needed to allow 425 * Firefox's support to properly function. These preferences will be 426 * automatically copied over to prefs.js during startup of Firefox. To be 427 * able to restore the original values of preferences a backup of prefs.js 428 * will be created. 429 */ 430 async function syncPreferences(options: ProfileOptions): Promise<void> { 431 const prefsPath = path.join(options.path, 'prefs.js'); 432 const userPath = path.join(options.path, 'user.js'); 433 434 const lines = Object.entries(options.preferences).map(([key, value]) => { 435 return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`; 436 }); 437 438 // Use allSettled to prevent corruption. 439 const result = await Promise.allSettled([ 440 backupFile(userPath).then(async () => { 441 await fs.promises.writeFile(userPath, lines.join('\n')); 442 }), 443 backupFile(prefsPath), 444 ]); 445 for (const command of result) { 446 if (command.status === 'rejected') { 447 throw command.reason; 448 } 449 } 450 } 451 452 export function compareVersions(a: string, b: string): number { 453 // TODO: this is a not very reliable check. 454 return parseInt(a.replace('.', ''), 16) - parseInt(b.replace('.', ''), 16); 455 }