diff --git a/src/firefox/index.js b/src/firefox/index.js index 756fe42fd3..c3adff970b 100644 --- a/src/firefox/index.js +++ b/src/firefox/index.js @@ -189,6 +189,90 @@ export async function run( } +// isDefaultProfile types and implementation. + +const DEFAULT_PROFILES_NAMES = [ + 'default', + 'dev-edition-default', +]; + +export type IsDefaultProfileFn = ( + profilePathOrName: string, + ProfileFinder?: typeof FirefoxProfile.Finder, + fsStat?: typeof fs.stat, +) => Promise; + +/* + * Tests if a profile is a default Firefox profile (both as a profile name or + * profile path). + * + * Returns a promise that resolves to true if the profile is one of default Firefox profile. + */ +export async function isDefaultProfile( + profilePathOrName: string, + ProfileFinder?: typeof FirefoxProfile.Finder = FirefoxProfile.Finder, + fsStat?: typeof fs.stat = fs.stat, +): Promise { + if (DEFAULT_PROFILES_NAMES.includes(profilePathOrName)) { + return true; + } + + const baseProfileDir = ProfileFinder.locateUserDirectory(); + const profilesIniPath = path.join(baseProfileDir, 'profiles.ini'); + try { + await fsStat(profilesIniPath); + } catch (error) { + if (isErrorWithCode('ENOENT', error)) { + log.debug(`profiles.ini not found: ${error}`); + + // No profiles exist yet, default to false (the default profile name contains a + // random generated component). + return false; + } + + // Re-throw any unexpected exception. + throw error; + } + + // Check for profile dir path. + const finder = new ProfileFinder(baseProfileDir); + const readProfiles = promisify(finder.readProfiles, finder); + + await readProfiles(); + + const normalizedProfileDirPath = path.normalize( + path.join(path.resolve(profilePathOrName), path.sep) + ); + + for (const profile of finder.profiles) { + // Check if the profile dir path or name is one of the default profiles + // defined in the profiles.ini file. + if (DEFAULT_PROFILES_NAMES.includes(profile.Name) || + profile.Default === '1') { + let profileFullPath; + + // Check for profile name. + if (profile.Name === profilePathOrName) { + return true; + } + + // Check for profile path. + if (profile.IsRelative === '1') { + profileFullPath = path.join(baseProfileDir, profile.Path, path.sep); + } else { + profileFullPath = path.join(profile.Path, path.sep); + } + + if (path.normalize(profileFullPath) === normalizedProfileDirPath) { + return true; + } + } + } + + // Profile directory not found. + return false; +} + // configureProfile types and implementation. export type ConfigureProfileOptions = {| @@ -238,6 +322,7 @@ export function configureProfile( export type UseProfileParams = { app?: PreferencesAppName, configureThisProfile?: ConfigureProfileFn, + isFirefoxDefaultProfile?: IsDefaultProfileFn, customPrefs?: FirefoxPreferences, }; @@ -248,9 +333,19 @@ export async function useProfile( { app, configureThisProfile = configureProfile, + isFirefoxDefaultProfile = isDefaultProfile, customPrefs = {}, }: UseProfileParams = {}, ): Promise { + const isForbiddenProfile = await isFirefoxDefaultProfile(profilePath); + if (isForbiddenProfile) { + throw new UsageError( + 'Cannot use --keep-profile-changes on a default profile' + + ` ("${profilePath}")` + + ' because web-ext will make it insecure and unsuitable for daily use.' + + '\nSee https://github.com/mozilla/web-ext/issues/1005' + ); + } const profile = new FirefoxProfile({destinationDirectory: profilePath}); return await configureThisProfile(profile, {app, customPrefs}); } diff --git a/tests/unit/test-firefox/test.firefox.js b/tests/unit/test-firefox/test.firefox.js index 80363fa460..ed8112ffe5 100644 --- a/tests/unit/test-firefox/test.firefox.js +++ b/tests/unit/test-firefox/test.firefox.js @@ -35,6 +35,38 @@ function withBaseProfile(callback) { ); } +function createFakeProfileFinder(profilesDirPath) { + const FakeProfileFinder = sinon.spy((...args) => { + const finder = new FirefoxProfile.Finder(...args); + + sinon.spy(finder, 'readProfiles'); + + return finder; + }); + + FakeProfileFinder.locateUserDirectory = sinon.spy(() => { + return profilesDirPath; + }); + + return FakeProfileFinder; +} + +async function createFakeProfilesIni( + dirPath: string, profilesDefs: Array +): Promise { + let content = ''; + + for (const [idx, profile] of profilesDefs.entries()) { + content += `[Profile${idx}]\n`; + for (const k of Object.keys(profile)) { + content += `${k}=${profile[k]}\n`; + } + content += '\n'; + } + + await fs.writeFile(path.join(dirPath, 'profiles.ini'), content); +} + describe('firefox', () => { describe('run', () => { @@ -263,6 +295,160 @@ describe('firefox', () => { }); + describe('isDefaultProfile', () => { + + it('detects common Firefox default profiles specified by name', + async () => { + const isDefault = await firefox.isDefaultProfile('default'); + assert.equal(isDefault, true); + + const isDevEditionDefault = await firefox.isDefaultProfile( + 'dev-edition-default' + ); + assert.equal(isDevEditionDefault, true); + }); + + it('allows profile name if it is not listed as default in profiles.ini', + async () => { + return withTempDir(async (tmpDir) => { + const profilesDirPath = tmpDir.path(); + const FakeProfileFinder = createFakeProfileFinder(profilesDirPath); + + await createFakeProfilesIni(profilesDirPath, [ + { + Name: 'manually-set-default', + Path: 'fake-default-profile', + IsRelative: 1, + Default: 1, + }, + ]); + + const isDefault = await firefox.isDefaultProfile( + 'manually-set-default', FakeProfileFinder + ); + assert.equal( + isDefault, true, + 'Manually configured default profile' + ); + + const isNotDefault = await firefox.isDefaultProfile( + 'unkown-profile-name', FakeProfileFinder + ); + assert.equal( + isNotDefault, false, + 'Unknown profile name' + ); + }); + }); + + it('allows profile path if it is not listed as default in profiles.ini', + async () => { + return withTempDir(async (tmpDir) => { + const profilesDirPath = tmpDir.path(); + const FakeProfileFinder = createFakeProfileFinder(profilesDirPath); + const absProfilePath = path.join( + profilesDirPath, + 'fake-manually-default-profile' + ); + + await createFakeProfilesIni(profilesDirPath, [ + { + Name: 'default', + Path: 'fake-default-profile', + IsRelative: 1, + }, + { + Name: 'dev-edition-default', + Path: 'fake-devedition-default-profile', + IsRelative: 1, + }, + { + Name: 'manually-set-default', + Path: absProfilePath, + Default: 1, + }, + ]); + + const isFirefoxDefaultPath = await firefox.isDefaultProfile( + path.join(profilesDirPath, 'fake-default-profile'), + FakeProfileFinder + ); + assert.equal( + isFirefoxDefaultPath, true, + 'Firefox default profile' + ); + + const isDevEditionDefaultPath = await firefox.isDefaultProfile( + path.join(profilesDirPath, 'fake-devedition-default-profile'), + FakeProfileFinder + ); + assert.equal( + isDevEditionDefaultPath, true, + 'Firefox DevEdition default profile' + ); + + const isManuallyDefault = await firefox.isDefaultProfile( + absProfilePath, + FakeProfileFinder + ); + assert.equal( + isManuallyDefault, true, + 'Manually configured default profile' + ); + + const isNotDefault = await firefox.isDefaultProfile( + path.join(profilesDirPath, 'unkown-profile-dir'), + FakeProfileFinder + ); + assert.equal( + isNotDefault, false, + 'Unknown profile path' + ); + }); + }); + + it('allows profile path if there is no profiles.ini file', + async () => { + return withTempDir(async (tmpDir) => { + const profilesDirPath = tmpDir.path(); + const FakeProfileFinder = createFakeProfileFinder(profilesDirPath); + + const isNotDefault = await firefox.isDefaultProfile( + '/tmp/my-custom-profile-dir', + FakeProfileFinder + ); + + assert.equal(isNotDefault, false); + }); + }); + + it('rejects on any unexpected error while looking for profiles.ini', + async () => { + return withTempDir(async (tmpDir) => { + const profilesDirPath = tmpDir.path(); + const FakeProfileFinder = createFakeProfileFinder(profilesDirPath); + const fakeFsStat = sinon.spy(() => { + return Promise.reject(new Error('Fake fs stat error')); + }); + + let exception; + try { + await firefox.isDefaultProfile( + '/tmp/my-custom-profile-dir', + FakeProfileFinder, + fakeFsStat + ); + } catch (error) { + exception = error; + } + + assert.match(exception && exception.message, /Fake fs stat error/); + }); + } + ); + + }); + describe('createProfile', () => { it('resolves with a profile object', () => { @@ -304,6 +490,30 @@ describe('firefox', () => { }); describe('useProfile', () => { + it('rejects to a UsageError when used on a default Firefox profile', + async () => { + const configureThisProfile = sinon.spy( + (profile) => Promise.resolve(profile) + ); + const isFirefoxDefaultProfile = sinon.spy( + () => Promise.resolve(true) + ); + let exception; + + try { + await firefox.useProfile('default', { + configureThisProfile, + isFirefoxDefaultProfile, + }); + } catch (error) { + exception = error; + } + + assert.match( + exception && exception.message, + /Cannot use --keep-profile-changes on a default profile/ + ); + }); it('resolves to a FirefoxProfile instance', () => withBaseProfile( (baseProfile) => {