diff --git a/cmd/maintained-apps/validate/darwin.go b/cmd/maintained-apps/validate/darwin.go index 08e89c6602..1eef53d20b 100644 --- a/cmd/maintained-apps/validate/darwin.go +++ b/cmd/maintained-apps/validate/darwin.go @@ -294,6 +294,15 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueAppIdent } } + // The Developer Edition cask version is the full beta ("153.0b13") but + // the bundle reports only the base version ("153.0"); accept base+"b". + if uniqueAppIdentifier == "org.mozilla.firefoxdeveloperedition" { + if result.Version != "" && strings.HasPrefix(appVersion, result.Version+"b") { + logger.InfoContext(ctx, "Firefox Developer Edition detected - cask version matches bundle base version with beta suffix") + return true, nil + } + } + // Check various version matching strategies if checkVersionMatch(appVersion, result.Version, result.BundledVersion) { return true, nil diff --git a/ee/maintained-apps/ingesters/homebrew/ingester.go b/ee/maintained-apps/ingesters/homebrew/ingester.go index c346a487aa..6e53374bd5 100644 --- a/ee/maintained-apps/ingesters/homebrew/ingester.go +++ b/ee/maintained-apps/ingesters/homebrew/ingester.go @@ -1,6 +1,7 @@ package homebrew import ( + "bytes" "context" "encoding/json" "errors" @@ -11,6 +12,7 @@ import ( "net/url" "os" "path" + "regexp" "strings" "time" @@ -33,6 +35,7 @@ func IngestApps(ctx context.Context, logger *slog.Logger, inputsPath, slugFilter i := &brewIngester{ baseURL: baseBrewAPIURL, + buildhubURL: buildhubAPIURL, logger: logger, client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)), retryInterval: 2 * time.Second, @@ -91,12 +94,17 @@ func IngestApps(ctx context.Context, logger *slog.Logger, inputsPath, slugFilter return manifestApps, nil } -const baseBrewAPIURL = "https://formulae.brew.sh/api/" +const ( + baseBrewAPIURL = "https://formulae.brew.sh/api/" + // buildhubAPIURL is Mozilla's build metadata search API. + buildhubAPIURL = "https://buildhub.moz.tools/api/search" +) type brewIngester struct { - baseURL string - logger *slog.Logger - client *http.Client + baseURL string + buildhubURL string + logger *slog.Logger + client *http.Client // retryInterval and retryMaxAttempts control retries of transient brew API // failures (network errors and 5xx/429 responses). formulae.brew.sh is @@ -239,10 +247,195 @@ func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintain out.UniqueIdentifier, out.Version, ) } + if input.Token == "firefox@developer-edition" { + // The bundle reports only the base version ("153.0") for cask version + // "153.0b13", so compare CFBundleVersion (encodes the build date, resolved + // via buildhub) to distinguish betas; fall back to a cycle-granular + // base-version comparison if buildhub is unavailable. + column := "bundle_version" + patchVersion, err := i.firefoxDevEditionMacBundleVersion(ctx, out.Version) + if err != nil { + i.logger.WarnContext(ctx, "resolving Firefox Developer Edition bundle version failed; patch policy falls back to base-version comparison", "err", err.Error()) + column, patchVersion = "bundle_short_version", firefoxBetaBaseVersion(out.Version) + } + out.Queries.Patched = fmt.Sprintf( + "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(%s, '%s') < 0);", + out.UniqueIdentifier, column, patchVersion, + ) + } + if input.Token == "firefox@nightly" { + // Nightly's CFBundleShortVersionString ("154.0a1") is constant all cycle; + // derive CFBundleVersion from the cask version's build timestamp for + // day-level patch status. + bundleVersion, err := firefoxNightlyMacBundleVersion(cask.Version) + if err != nil { + i.logger.WarnContext(ctx, "deriving Firefox Nightly bundle version failed; patch policy falls back to short-version comparison", "err", err.Error()) + } else { + out.Queries.Patched = fmt.Sprintf( + "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_version, '%s') < 0);", + out.UniqueIdentifier, bundleVersion, + ) + } + } return out, nil } +var firefoxBetaVersionPattern = regexp.MustCompile(`^(\d+(?:\.\d+)*)b\d+$`) + +// firefoxBetaBaseVersion strips the beta suffix from a Firefox pre-release +// version ("153.0b13" -> "153.0"); non-matching versions pass through unchanged. +func firefoxBetaBaseVersion(version string) string { + if m := firefoxBetaVersionPattern.FindStringSubmatch(version); m != nil { + return m[1] + } + return version +} + +// firefoxMacBundleVersion computes a Firefox mac build's CFBundleVersion: +// "..", unpadded ("153.0b13" + "20260715" -> "15326.7.15"). +func firefoxMacBundleVersion(version, buildDate string) (string, error) { + major, _, _ := strings.Cut(version, ".") + if major == "" || strings.Trim(major, "0123456789") != "" { + return "", fmt.Errorf("cannot parse major version from %q", version) + } + if len(buildDate) < 8 { + return "", fmt.Errorf("invalid build date %q", buildDate) + } + date, err := time.Parse("20060102", buildDate[:8]) + if err != nil { + return "", fmt.Errorf("invalid build date %q", buildDate) + } + yy := buildDate[2:4] + return fmt.Sprintf("%s%s.%d.%d", major, yy, int(date.Month()), date.Day()), nil +} + +// firefoxNightlyCaskVersionPattern extracts the build timestamp from a Firefox +// Nightly cask version ("154.0a1,2026-07-17-09-27-13"). +var firefoxNightlyCaskVersionPattern = regexp.MustCompile(`^[^,]+,(\d{4})-(\d{2})-(\d{2})(?:-|$)`) + +// firefoxNightlyMacBundleVersion derives CFBundleVersion from a Nightly cask +// version ("154.0a1,2026-07-17-09-27-13" -> "15426.7.17"). +func firefoxNightlyMacBundleVersion(caskVersion string) (string, error) { + m := firefoxNightlyCaskVersionPattern.FindStringSubmatch(caskVersion) + if m == nil { + return "", fmt.Errorf("cask version %q has no build timestamp", caskVersion) + } + return firefoxMacBundleVersion(caskVersion, m[1]+m[2]+m[3]) +} + +// firefoxDevEditionMacBundleVersion resolves a Developer Edition mac build's +// CFBundleVersion ("153.0b13" -> "15326.7.15") by looking up its build id in +// buildhub, where DevEd is indexed as product "firefox", channel "aurora". +func (i *brewIngester) firefoxDevEditionMacBundleVersion(ctx context.Context, version string) (string, error) { + type term map[string]map[string]string + reqBody, err := json.Marshal(map[string]any{ + "size": 1, + "query": map[string]any{ + "bool": map[string]any{ + "must": []term{ + {"term": {"source.product": "firefox"}}, + {"term": {"target.channel": "aurora"}}, + {"term": {"target.platform": "mac"}}, + {"term": {"target.version": version}}, + }, + }, + }, + "sort": []term{{"build.id": {"order": "desc"}}}, + }) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "marshal buildhub query") + } + + interval := i.retryInterval + if interval <= 0 { + interval = 2 * time.Second + } + maxAttempts := i.retryMaxAttempts + if maxAttempts <= 0 { + maxAttempts = 5 + } + + var body []byte + attempt := 0 + err = retry.Do(func() error { + attempt++ + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, i.buildhubURL, bytes.NewReader(reqBody)) + if err != nil { + return ctxerr.Wrap(ctx, err, "create buildhub http request") + } + req.Header.Set("Content-Type", "application/json") + + res, err := i.client.Do(req) + if err != nil { + // Caller cancellation/deadline is not transient; stop retrying. + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + i.logger.WarnContext(ctx, "buildhub request failed, retrying", "attempt", attempt, "err", err.Error()) + return &transientErr{ctxerr.Wrap(ctx, err, "execute buildhub http request")} + } + defer res.Body.Close() + + body, err = io.ReadAll(res.Body) + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + i.logger.WarnContext(ctx, "reading buildhub response failed, retrying", "attempt", attempt, "err", err.Error()) + return &transientErr{ctxerr.Wrap(ctx, err, "read buildhub response body")} + } + + switch res.StatusCode { + case http.StatusOK: + return nil + case http.StatusTooManyRequests, + http.StatusInternalServerError, + http.StatusBadGateway, + http.StatusServiceUnavailable, + http.StatusGatewayTimeout: + i.logger.WarnContext(ctx, "buildhub returned transient error, retrying", "attempt", attempt, "status", res.StatusCode) + return &transientErr{ctxerr.Errorf(ctx, "buildhub returned status %d: %s", res.StatusCode, truncateBody(body))} + default: + return ctxerr.Errorf(ctx, "buildhub returned status %d: %s", res.StatusCode, truncateBody(body)) + } + }, + retry.WithInterval(interval), + retry.WithBackoffMultiplier(2), + retry.WithMaxAttempts(maxAttempts), + retry.WithErrorFilter(func(err error) retry.ErrorOutcome { + if _, ok := errors.AsType[*transientErr](err); ok { + return retry.ErrorOutcomeNormalRetry + } + return retry.ErrorOutcomeDoNotRetry + }), + ) + if err != nil { + return "", err + } + + var resp struct { + Hits struct { + Hits []struct { + Source struct { + Build struct { + ID string `json:"id"` + } `json:"build"` + } `json:"_source"` + } `json:"hits"` + } `json:"hits"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", ctxerr.Wrap(ctx, err, "unmarshal buildhub response") + } + if len(resp.Hits.Hits) == 0 { + return "", ctxerr.Errorf(ctx, "no buildhub build found for version %s", version) + } + + return firefoxMacBundleVersion(version, resp.Hits.Hits[0].Source.Build.ID) +} + // fetchCask resolves the brew cask JSON for the given input app from // either a local file (cask_path) or the default brew API. func (i *brewIngester) fetchCask(ctx context.Context, input inputApp) (brewCask, error) { diff --git a/ee/maintained-apps/ingesters/homebrew/ingester_test.go b/ee/maintained-apps/ingesters/homebrew/ingester_test.go index de9f138879..1fd5446022 100644 --- a/ee/maintained-apps/ingesters/homebrew/ingester_test.go +++ b/ee/maintained-apps/ingesters/homebrew/ingester_test.go @@ -95,6 +95,22 @@ func TestIngestValidations(t *testing.T) { Version: "1.0", } + case "firefox@developer-edition": + cask = brewCask{ + Token: appToken, + Name: []string{"Mozilla Firefox Developer Edition"}, + URL: "https://example.com", + Version: "153.0b13", + } + + case "firefox@nightly": + cask = brewCask{ + Token: appToken, + Name: []string{"Mozilla Firefox Nightly"}, + URL: "https://example.com", + Version: "154.0a1,2026-07-17-09-27-13", + } + default: w.WriteHeader(http.StatusBadRequest) t.Fatalf("unexpected app token %s", appToken) @@ -105,6 +121,12 @@ func TestIngestValidations(t *testing.T) { })) t.Cleanup(srv.Close) + // buildhub stub: DevEd 153.0b13 build id -> CFBundleVersion "15326.7.15". + buildhubSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"hits":{"hits":[{"_source":{"build":{"id":"20260715125817"}}}]}}`)) + })) + t.Cleanup(buildhubSrv.Close) + ctx := context.Background() cases := []struct { @@ -121,6 +143,8 @@ func TestIngestValidations(t *testing.T) { {"parse URL for cask invalidurl", inputApp{Token: "invalidurl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}}, {"", inputApp{Token: "ok", UniqueIdentifier: "abc", InstallerFormat: "pkg"}}, {"", inputApp{Token: "docker-desktop", UniqueIdentifier: "com.electron.dockerdesktop", InstallerFormat: "dmg", Name: "Docker Desktop", Slug: "docker-desktop/darwin"}}, + {"", inputApp{Token: "firefox@developer-edition", UniqueIdentifier: "org.mozilla.firefoxdeveloperedition", InstallerFormat: "dmg", Name: "Mozilla Firefox Developer Edition", Slug: "firefox@developer-edition/darwin"}}, + {"", inputApp{Token: "firefox@nightly", UniqueIdentifier: "org.mozilla.nightly", InstallerFormat: "dmg", Name: "Mozilla Firefox Nightly", Slug: "firefox@nightly/darwin"}}, {"", inputApp{Token: "swiftdialog", UniqueIdentifier: "au.csiro.dialog", InstallerFormat: "pkg", Name: "swiftDialog", Slug: "swiftdialog/darwin"}}, {"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}}, {"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}}, @@ -133,6 +157,7 @@ func TestIngestValidations(t *testing.T) { logger: slog.New(slog.DiscardHandler), client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)), baseURL: srv.URL + "/", + buildhubURL: buildhubSrv.URL, retryInterval: time.Millisecond, retryMaxAttempts: 3, } @@ -160,6 +185,21 @@ func TestIngestValidations(t *testing.T) { "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.electron.dockerdesktop' AND path NOT LIKE '%.back' AND version_compare(bundle_short_version, '1.0') < 0);", out.Queries.Patched, ) + case "firefox@developer-edition": + // Patched query compares the buildhub-resolved CFBundleVersion. + require.Equal(t, "153.0b13", out.Version) + require.Equal(t, + "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_version, '15326.7.15') < 0);", + out.Queries.Patched, + ) + case "firefox@nightly": + // Patched query compares the CFBundleVersion derived from the cask + // version's build timestamp. + require.Equal(t, "154.0a1", out.Version) + require.Equal(t, + "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly' AND version_compare(bundle_version, '15426.7.17') < 0);", + out.Queries.Patched, + ) case "swiftdialog": require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'au.csiro.dialog' AND path != '/opt/orbit/bin/swiftDialog/macos/stable/Dialog.app';", out.Queries.Exists) require.Equal(t, @@ -361,3 +401,131 @@ func TestIngestCaskPath(t *testing.T) { require.ErrorContains(t, err, "empty name") require.Equal(t, 0, httpHits) } + +func TestFirefoxBetaBaseVersion(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"153.0b13", "153.0"}, + {"154.0b1", "154.0"}, + {"153.0.1b2", "153.0.1"}, + {"153.0", "153.0"}, + {"152.0.6", "152.0.6"}, + {"154.0a1", "154.0a1"}, + {"", ""}, + } + for _, c := range cases { + require.Equal(t, c.want, firefoxBetaBaseVersion(c.in), "input %q", c.in) + } +} + +func TestFirefoxMacBundleVersion(t *testing.T) { + cases := []struct { + version string + buildDate string + want string + wantErr bool + }{ + {"153.0b13", "20260715125817", "15326.7.15", false}, + {"154.0a1", "20260717", "15426.7.17", false}, + {"153.0.1b2", "20261201000000", "15326.12.1", false}, + {"153.0b13", "2026071", "", true}, // build date too short + {"153.0b13", "2026x715", "", true}, // build date not numeric + {"153.0b13", "20261315000000", "", true}, // month out of range + {"153.0b13", "20260732000000", "", true}, // day out of range + {"153.0b13", "20260231000000", "", true}, // impossible calendar date + {"x.0b13", "20260715125817", "", true}, // non-numeric major + {"", "20260715125817", "", true}, + } + for _, c := range cases { + got, err := firefoxMacBundleVersion(c.version, c.buildDate) + if c.wantErr { + require.Error(t, err, "version %q buildDate %q", c.version, c.buildDate) + continue + } + require.NoError(t, err, "version %q buildDate %q", c.version, c.buildDate) + require.Equal(t, c.want, got, "version %q buildDate %q", c.version, c.buildDate) + } +} + +func TestFirefoxNightlyMacBundleVersion(t *testing.T) { + cases := []struct { + caskVersion string + want string + wantErr bool + }{ + {"154.0a1,2026-07-17-09-27-13", "15426.7.17", false}, + {"154.0a1,2026-07-17", "15426.7.17", false}, + {"154.0a1", "", true}, // no build timestamp + {"154.0a1,not-a-date", "", true}, // malformed timestamp + {"154.0a1,2026-13-17-09-27-13", "", true}, // month out of range + } + for _, c := range cases { + got, err := firefoxNightlyMacBundleVersion(c.caskVersion) + if c.wantErr { + require.Error(t, err, "caskVersion %q", c.caskVersion) + continue + } + require.NoError(t, err, "caskVersion %q", c.caskVersion) + require.Equal(t, c.want, got, "caskVersion %q", c.caskVersion) + } +} + +// TestFirefoxDevEditionBuildhubFallback verifies that a buildhub failure falls +// back to a base-version patch comparison instead of failing ingestion. +func TestFirefoxDevEditionBuildhubFallback(t *testing.T) { + brewSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(brewCask{ + Token: "firefox@developer-edition", + Name: []string{"Mozilla Firefox Developer Edition"}, + URL: "https://example.com", + Version: "153.0b13", + }) + if err != nil { + t.Errorf("encoding fixture: %v", err) + } + })) + t.Cleanup(brewSrv.Close) + + cases := []struct { + name string + handler http.HandlerFunc + }{ + {"buildhub has no matching build", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"hits":{"hits":[]}}`)) + }}, + {"buildhub is unavailable", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + }}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + buildhubSrv := httptest.NewServer(c.handler) + t.Cleanup(buildhubSrv.Close) + + i := &brewIngester{ + logger: slog.New(slog.DiscardHandler), + client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)), + baseURL: brewSrv.URL + "/", + buildhubURL: buildhubSrv.URL, + retryInterval: time.Millisecond, + retryMaxAttempts: 2, + } + + out, err := i.ingestOne(context.Background(), inputApp{ + Token: "firefox@developer-edition", + UniqueIdentifier: "org.mozilla.firefoxdeveloperedition", + InstallerFormat: "dmg", + Name: "Mozilla Firefox Developer Edition", + Slug: "firefox@developer-edition/darwin", + }) + require.NoError(t, err) + require.Equal(t, "153.0b13", out.Version) + require.Equal(t, + "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_short_version, '153.0') < 0);", + out.Queries.Patched, + ) + }) + } +} diff --git a/ee/maintained-apps/inputs/homebrew/firefox@developer-edition.json b/ee/maintained-apps/inputs/homebrew/firefox@developer-edition.json new file mode 100644 index 0000000000..e7898efbae --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/firefox@developer-edition.json @@ -0,0 +1,8 @@ +{ + "name": "Mozilla Firefox Developer Edition", + "slug": "firefox@developer-edition/darwin", + "unique_identifier": "org.mozilla.firefoxdeveloperedition", + "token": "firefox@developer-edition", + "installer_format": "dmg", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/homebrew/firefox@nightly.json b/ee/maintained-apps/inputs/homebrew/firefox@nightly.json new file mode 100644 index 0000000000..e2701fcd07 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/firefox@nightly.json @@ -0,0 +1,8 @@ +{ + "name": "Mozilla Firefox Nightly", + "slug": "firefox@nightly/darwin", + "unique_identifier": "org.mozilla.nightly", + "token": "firefox@nightly", + "installer_format": "dmg", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/winget/firefox@developer-edition.json b/ee/maintained-apps/inputs/winget/firefox@developer-edition.json new file mode 100644 index 0000000000..21b9ead460 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/firefox@developer-edition.json @@ -0,0 +1,12 @@ +{ + "name": "Mozilla Firefox Developer Edition", + "slug": "firefox@developer-edition/windows", + "package_identifier": "Mozilla.Firefox.DeveloperEdition", + "unique_identifier": "Firefox Developer Edition (x64 en-US)", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "machine", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/winget/firefox@nightly.json b/ee/maintained-apps/inputs/winget/firefox@nightly.json new file mode 100644 index 0000000000..41d23febd5 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/firefox@nightly.json @@ -0,0 +1,13 @@ +{ + "name": "Mozilla Firefox Nightly", + "slug": "firefox@nightly/windows", + "package_identifier": "Mozilla.Firefox.Nightly.MSIX", + "unique_identifier": "Firefox Nightly", + "program_publisher": "Mozilla Corporation", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_nightly_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_nightly_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "msix", + "installer_scope": "user", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_install.ps1 new file mode 100644 index 0000000000..d2fd8d6a8d --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_install.ps1 @@ -0,0 +1,27 @@ +# Learn more about .exe install scripts: +# http://fleetdm.com/learn-more-about/exe-install-scripts + +$exeFilePath = "${env:INSTALLER_PATH}" + +try { + +# Firefox's full installer is NSIS-based; /S installs silently and machine-wide. +$processOptions = @{ + FilePath = "$exeFilePath" + ArgumentList = "/S" + PassThru = $true + Wait = $true +} + +# Start process and track exit code +$process = Start-Process @processOptions +$exitCode = $process.ExitCode + +# Prints the exit code +Write-Host "Install exit code: $exitCode" +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_uninstall.ps1 new file mode 100644 index 0000000000..575d4f54e1 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_developer_edition_uninstall.ps1 @@ -0,0 +1,90 @@ +# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID +# variable +$softwareName = "Firefox Developer Edition" + +# Developer Edition registers as "Firefox Developer Edition (x64 en-US)"; the +# prefix match cannot hit the release, ESR, or Nightly entries. +$softwareNameLike = "$softwareName*" + +# Firefox's NSIS uninstaller (helper.exe) runs silently with /S. +$uninstallArgs = "/S" + +$machineKey = ` + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' +$machineKey32on64 = ` + 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' + +$exitCode = 0 + +try { + +[array]$uninstallKeys = Get-ChildItem ` + -Path @($machineKey, $machineKey32on64) ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath } + +$foundUninstaller = $false +foreach ($key in $uninstallKeys) { + # If needed, add -notlike to the comparison to exclude certain similar + # software + if ($key.DisplayName -like $softwareNameLike) { + $foundUninstaller = $true + # Get the uninstall command. Some uninstallers do not include + # 'QuietUninstallString' and require a flag to run silently. + $uninstallCommand = if ($key.QuietUninstallString) { + $key.QuietUninstallString + } else { + $key.UninstallString + } + + # The uninstall command may contain command and args, like: + # "C:\Program Files\Software\uninstall.exe" --uninstall --silent + # Split the command and args + $splitArgs = $uninstallCommand.Split('"') + if ($splitArgs.Length -gt 1) { + if ($splitArgs.Length -eq 3) { + $uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim() + } elseif ($splitArgs.Length -gt 3) { + Throw ` + "Uninstall command contains multiple quoted strings. " + + "Please update the uninstall script.`n" + + "Uninstall command: $uninstallCommand" + } + $uninstallCommand = $splitArgs[1] + } + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $processOptions = @{ + FilePath = $uninstallCommand + PassThru = $true + Wait = $true + } + if ($uninstallArgs -ne '') { + $processOptions.ArgumentList = "$uninstallArgs" + } + + # Start process and track exit code + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + + # Prints the exit code + Write-Host "Uninstall exit code: $exitCode" + # Exit the loop once the software is found and uninstalled. + break + } +} + +if (-not $foundUninstaller) { + Write-Host "Uninstaller for '$softwareName' not found." + # Change exit code to 0 if you don't want to fail if uninstaller is not + # found. This could happen if program was already uninstalled. + $exitCode = 1 +} + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} + +Exit $exitCode diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_install.ps1 new file mode 100644 index 0000000000..b66a74f26d --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_install.ps1 @@ -0,0 +1,99 @@ +# MSIX: provision machine-wide so the app is available to all users at sign-in, then +# opportunistically register for the currently logged-on console user (via a scheduled +# task in their session) so the app is immediately visible without requiring sign-out. +# +# The Fleet agent runs as Local System on Windows, and Add-AppxPackage cannot run in that +# context (HRESULT 0x80073CF9). The scheduled task is the supported way to register a +# package in a user session from a system-context script. + +$softwareName = "FirefoxNightly" +$taskName = "fleet-install-$softwareName.msix" +$scriptPath = "$env:PUBLIC\install-$softwareName.ps1" +$exitCodeFile = "$env:PUBLIC\install-exitcode-$softwareName.txt" + +try { + + $msixPath = $env:INSTALLER_PATH + if (-not $msixPath) { + throw "INSTALLER_PATH is not set" + } + + Write-Host "Provisioning MSIX for all users..." + $result = Add-AppxProvisionedPackage -Online -PackagePath $msixPath -SkipLicense -Regions "all" -ErrorAction Stop + $result | Out-String | Write-Host + + # Win32_ComputerSystem.UserName returns the console user (DOMAIN\User) or null when no + # interactive session is active. Other RDP/fast-user-switch sessions won't get the + # immediate registration; those users will pick it up from the provisioned install at + # their next sign-in. + $userName = (Get-CimInstance Win32_ComputerSystem).UserName + if (-not $userName -or $userName -notlike "*\*") { + Write-Host "No interactive user logged on; provisioned install will register for each user at sign-in." + Start-Sleep -Seconds 5 + Exit 0 + } + + Write-Host "Registering MSIX for logged-on user '$userName' via scheduled task..." + + $userScript = @" +`$msixPath = "$msixPath" +`$exitCodeFile = "$exitCodeFile" +try { + Add-AppxPackage -Path `$msixPath -ErrorAction Stop | Out-String | Write-Host + Set-Content -Path `$exitCodeFile -Value 0 +} catch { + Write-Host "Add-AppxPackage failed: `$(`$_.Exception.Message)" + Set-Content -Path `$exitCodeFile -Value 1 +} +"@ + + Set-Content -Path $scriptPath -Value $userScript -Force + + $action = New-ScheduledTaskAction -Execute "powershell.exe" ` + -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$scriptPath`"" + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries + $principal = New-ScheduledTaskPrincipal -UserId $userName -RunLevel Highest + $task = New-ScheduledTask -Action $action -Settings $settings -Principal $principal + Register-ScheduledTask -TaskName $taskName -InputObject $task -User $userName -Force | Out-Null + Start-ScheduledTask -TaskName $taskName + + $startDate = Get-Date + $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State + while ($state -ne "Running") { + Start-Sleep -Seconds 1 + if ((New-Timespan -Start $startDate).TotalSeconds -gt 30) { + Write-Host "Per-user registration task did not start within 30s; provisioned install is still valid." + break + } + $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State + } + + while ($state -eq "Running") { + Start-Sleep -Seconds 2 + if ((New-Timespan -Start $startDate).TotalSeconds -gt 90) { + Write-Host "Per-user registration task did not complete within 90s; provisioned install is still valid." + break + } + $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State + } + + if (Test-Path $exitCodeFile) { + $code = (Get-Content $exitCodeFile -ErrorAction SilentlyContinue | Select-Object -First 1).Trim() + if ($code -eq "0") { + Write-Host "Per-user registration completed for '$userName'." + } else { + Write-Host "Per-user registration did not complete cleanly (exit code: $code). Provisioned install is still valid." + } + } + + Start-Sleep -Seconds 5 + Exit 0 + +} catch { + Write-Host "Error: $_" + Exit 1 +} finally { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue + Remove-Item -Path $exitCodeFile -Force -ErrorAction SilentlyContinue +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_uninstall.ps1 new file mode 100644 index 0000000000..fc677d11da --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_nightly_uninstall.ps1 @@ -0,0 +1,53 @@ +$timeoutSeconds = 300 # 5 minute timeout + +# Match only the Nightly channel: its MSIX identity "Mozilla.MozillaFirefoxNightly" +# cannot collide with other Firefox channels' identities. Don't match on a +# PackageFamilyName property: Get-AppxProvisionedPackage doesn't expose it, so an +# "-eq" match is $null for every package. +function ShouldRemoveFirefoxNightlyPackage { + param([Parameter(Mandatory=$true)]$pkg) + try { + $name = [string]$pkg.Name + $family = [string]$pkg.PackageFamilyName + + if ($name -and ($name -like "*MozillaFirefoxNightly*")) { return $true } + if ($family -and ($family -like "*MozillaFirefoxNightly*")) { return $true } + } catch {} + return $false +} + +try { + + $start = Get-Date + + $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object { + ($_.DisplayName -and ($_.DisplayName -like "*MozillaFirefoxNightly*")) -or + ($_.PackageName -and ($_.PackageName -like "*MozillaFirefoxNightly*")) + } + foreach ($pkg in $provisioned) { + Write-Host "Removing provisioned package: $($pkg.PackageName)" + Remove-AppxProvisionedPackage -Online -PackageName $pkg.PackageName -AllUsers -ErrorAction Stop | Out-String | Write-Host + $elapsed = (New-TimeSpan -Start $start).TotalSeconds + if ($elapsed -gt $timeoutSeconds) { + Exit 1603 + } + } + + $installed = Get-AppxPackage -AllUsers -PackageTypeFilter Main -ErrorAction SilentlyContinue | Where-Object { + ShouldRemoveFirefoxNightlyPackage $_ + } + foreach ($app in $installed) { + Write-Host "Removing installed package: $($app.PackageFullName)" + Remove-AppxPackage -Package $app.PackageFullName -AllUsers -ErrorAction Stop | Out-String | Write-Host + $elapsed = (New-TimeSpan -Start $start).TotalSeconds + if ($elapsed -gt $timeoutSeconds) { + Exit 1603 + } + } + + Exit 0 + +} catch { + Write-Host "Error: $_" + Exit 1603 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_uninstall.ps1 index 9d4e58e2d7..3e8b0f2a95 100644 --- a/ee/maintained-apps/inputs/winget/scripts/firefox_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_uninstall.ps1 @@ -1,10 +1,11 @@ # Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID # variable -$softwareName = "Firefox" +$softwareName = "Mozilla Firefox" -# It is recommended to use exact software name here if possible to avoid -# uninstalling unintended software. -$softwareNameLike = "*$softwareName*" +# Match only the release channel ("Mozilla Firefox (x64 en-US)"); the prefix +# match plus the ESR exclusion below keeps ESR, Developer Edition, and Nightly +# entries untouched. +$softwareNameLike = "$softwareName*" # Some uninstallers require a flag to run silently. # Each uninstaller might use different argument (usually it's "/S" or "/s") @@ -28,7 +29,8 @@ $foundUninstaller = $false foreach ($key in $uninstallKeys) { # If needed, add -notlike to the comparison to exclude certain similar # software - if ($key.DisplayName -like $softwareNameLike) { + if ($key.DisplayName -like $softwareNameLike -and + $key.DisplayName -notlike "*ESR*") { $foundUninstaller = $true # Get the uninstall command. Some uninstallers do not include # 'QuietUninstallString' and require a flag to run silently. diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index cf93d335b4..84cae33ea7 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -3277,6 +3277,20 @@ "unique_identifier": "Mozilla Firefox (x64 en-US)", "description": "Firefox is a powerful, open-source web browser built for speed, privacy, and customization." }, + { + "name": "Mozilla Firefox Developer Edition", + "slug": "firefox@developer-edition/darwin", + "platform": "darwin", + "unique_identifier": "org.mozilla.firefoxdeveloperedition", + "description": "Mozilla Firefox Developer Edition is the version of the Firefox web browser made for web developers, with cutting-edge features and built-in developer tools." + }, + { + "name": "Mozilla Firefox Developer Edition", + "slug": "firefox@developer-edition/windows", + "platform": "windows", + "unique_identifier": "Firefox Developer Edition (x64 en-US)", + "description": "Mozilla Firefox Developer Edition is the version of the Firefox web browser made for web developers, with cutting-edge features and built-in developer tools." + }, { "name": "Mozilla Firefox ESR", "slug": "firefox@esr/darwin", @@ -3291,6 +3305,20 @@ "unique_identifier": "Mozilla Firefox 140.7.1 ESR (x64 en-US)", "description": "Mozilla Firefox ESR is the Extended Support Release version of the popular web browser Firefox." }, + { + "name": "Mozilla Firefox Nightly", + "slug": "firefox@nightly/darwin", + "platform": "darwin", + "unique_identifier": "org.mozilla.nightly", + "description": "Mozilla Firefox Nightly is the daily development build of the Firefox web browser, with the newest features before they reach beta and release." + }, + { + "name": "Mozilla Firefox Nightly", + "slug": "firefox@nightly/windows", + "platform": "windows", + "unique_identifier": "Firefox Nightly", + "description": "Mozilla Firefox Nightly is the daily development build of the Firefox web browser, with the newest features before they reach beta and release." + }, { "name": "Fission", "slug": "fission/darwin", diff --git a/ee/maintained-apps/outputs/firefox/windows.json b/ee/maintained-apps/outputs/firefox/windows.json index bcaad3ad25..bc4d18f5c0 100644 --- a/ee/maintained-apps/outputs/firefox/windows.json +++ b/ee/maintained-apps/outputs/firefox/windows.json @@ -8,7 +8,7 @@ }, "installer_url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/152.0.6/win64/en-US/Firefox%20Setup%20152.0.6.exe", "install_script_ref": "80fb9175", - "uninstall_script_ref": "8b5e20e4", + "uninstall_script_ref": "ae547434", "sha256": "3d4fcc5370bb183c9535d64b98d946c2dacf664a394ebcc02844e361d58d59b4", "default_categories": [ "Browsers" @@ -17,6 +17,6 @@ ], "refs": { "80fb9175": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Add argument to install silently\n# Argument to make install silent depends on installer,\n# each installer might use different argument (usually it's \"/S\" or \"/s\")\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n \n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Add arguments to install silently (Firefox uses an Inno Setup-based installer)\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /MERGETASKS=!runcode\"\n PassThru = $true\n Wait = $true\n}\n \n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", - "8b5e20e4": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Firefox\"\n\n# It is recommended to use exact software name here if possible to avoid\n# uninstalling unintended software.\n$softwareNameLike = \"*$softwareName*\"\n\n# Some uninstallers require a flag to run silently.\n# Each uninstaller might use different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n" + "ae547434": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Mozilla Firefox\"\n\n# Match only the release channel (\"Mozilla Firefox (x64 en-US)\"); the prefix\n# match plus the ESR exclusion below keeps ESR, Developer Edition, and Nightly\n# entries untouched.\n$softwareNameLike = \"$softwareName*\"\n\n# Some uninstallers require a flag to run silently.\n# Each uninstaller might use different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike -and\n $key.DisplayName -notlike \"*ESR*\") {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n" } } diff --git a/ee/maintained-apps/outputs/firefox@developer-edition/darwin.json b/ee/maintained-apps/outputs/firefox@developer-edition/darwin.json new file mode 100644 index 0000000000..7b6e8a5bec --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@developer-edition/darwin.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "153.0b13", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefoxdeveloperedition' AND version_compare(bundle_version, '15326.7.15') < 0);" + }, + "installer_url": "https://download-installer.cdn.mozilla.net/pub/devedition/releases/153.0b13/mac/en-US/Firefox%20153.0b13.dmg", + "install_script_ref": "a313f903", + "uninstall_script_ref": "b108b0ff", + "sha256": "6d30d4c74a97ed2e0fea2956006a1e0d589a856061291eb8bbde95378cc806fd", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "a313f903": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'org.mozilla.firefoxdeveloperedition'\nif [ -d \"$APPDIR/Firefox Developer Edition.app\" ]; then\n\tsudo mv \"$APPDIR/Firefox Developer Edition.app\" \"$TMPDIR/Firefox Developer Edition.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Firefox Developer Edition.app\" \"$APPDIR\"\nrelaunch_application 'org.mozilla.firefoxdeveloperedition'\n", + "b108b0ff": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nsudo rm -rf \"$APPDIR/Firefox Developer Edition.app\"\nsudo rmdir '~/Library/Application Support/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla/updates'\nsudo rmdir '~/Library/Caches/Mozilla/updates/Applications'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox'\n" + } +} diff --git a/ee/maintained-apps/outputs/firefox@developer-edition/windows.json b/ee/maintained-apps/outputs/firefox@developer-edition/windows.json new file mode 100644 index 0000000000..a0149f43df --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@developer-edition/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "151.0", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Firefox Developer Edition (x64 en-US)' AND publisher = 'Mozilla';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Firefox Developer Edition (x64 en-US)' AND publisher = 'Mozilla' AND version_compare(version, '151.0') < 0);" + }, + "installer_url": "https://download-installer.cdn.mozilla.net/pub/devedition/releases/151.0b10/win64/en-US/Firefox%20Setup%20151.0b10.exe", + "install_script_ref": "30fd5964", + "uninstall_script_ref": "cc59b3f5", + "sha256": "ef21f97a29de39f55882368e22189c32c289518e9063acf89d1eb28b12f31023", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "30fd5964": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Firefox's full installer is NSIS-based; /S installs silently and machine-wide.\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n\n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "cc59b3f5": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = \"Firefox Developer Edition\"\n\n# Developer Edition registers as \"Firefox Developer Edition (x64 en-US)\"; the\n# prefix match cannot hit the release, ESR, or Nightly entries.\n$softwareNameLike = \"$softwareName*\"\n\n# Firefox's NSIS uninstaller (helper.exe) runs silently with /S.\n$uninstallArgs = \"/S\"\n\n$machineKey = `\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = `\n 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($machineKey, $machineKey32on64) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start process and track exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n # Change exit code to 0 if you don't want to fail if uninstaller is not\n # found. This could happen if program was already uninstalled.\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\nExit $exitCode\n" + } +} diff --git a/ee/maintained-apps/outputs/firefox@nightly/darwin.json b/ee/maintained-apps/outputs/firefox@nightly/darwin.json new file mode 100644 index 0000000000..e282e87d90 --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@nightly/darwin.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "154.0a1", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.nightly' AND version_compare(bundle_version, '15426.7.17') < 0);" + }, + "installer_url": "https://ftp.mozilla.org/pub/firefox/nightly/2026/07/2026-07-17-09-27-13-mozilla-central/firefox-154.0a1.en-US.mac.dmg", + "install_script_ref": "418c9331", + "uninstall_script_ref": "d7c711ad", + "sha256": "d2e4b8ce0eb19a9d5c99c86bf0b0c660130e4b3357e056da92844c3ce47bb982", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "418c9331": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'org.mozilla.nightly'\nif [ -d \"$APPDIR/Firefox Nightly.app\" ]; then\n\tsudo mv \"$APPDIR/Firefox Nightly.app\" \"$TMPDIR/Firefox Nightly.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Firefox Nightly.app\" \"$APPDIR\"\nrelaunch_application 'org.mozilla.nightly'\n", + "d7c711ad": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nsudo rm -rf \"$APPDIR/Firefox Nightly.app\"\nsudo rmdir '~/Library/Application Support/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla/updates'\nsudo rmdir '~/Library/Caches/Mozilla/updates/Applications'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox'\n" + } +} diff --git a/ee/maintained-apps/outputs/firefox@nightly/windows.json b/ee/maintained-apps/outputs/firefox@nightly/windows.json new file mode 100644 index 0000000000..801cb38a95 --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@nightly/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "154.2607.1709.0", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Firefox Nightly' AND publisher = 'Mozilla Corporation';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Firefox Nightly' AND publisher = 'Mozilla Corporation' AND version_compare(version, '154.2607.1709.0') < 0);" + }, + "installer_url": "https://ftp.mozilla.org/pub/firefox/nightly/2026/07/2026-07-17-09-27-13-mozilla-central/firefox-154.0a1.multi.win64.installer.msix", + "install_script_ref": "255e7c51", + "uninstall_script_ref": "f0d0fed1", + "sha256": "b9f11a4edf2929d08c179ea9fc86ccd57a7b7b0b111c3d9742dfb2629a8c6cea", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "255e7c51": "# MSIX: provision machine-wide so the app is available to all users at sign-in, then\n# opportunistically register for the currently logged-on console user (via a scheduled\n# task in their session) so the app is immediately visible without requiring sign-out.\n#\n# The Fleet agent runs as Local System on Windows, and Add-AppxPackage cannot run in that\n# context (HRESULT 0x80073CF9). The scheduled task is the supported way to register a\n# package in a user session from a system-context script.\n\n$softwareName = \"FirefoxNightly\"\n$taskName = \"fleet-install-$softwareName.msix\"\n$scriptPath = \"$env:PUBLIC\\install-$softwareName.ps1\"\n$exitCodeFile = \"$env:PUBLIC\\install-exitcode-$softwareName.txt\"\n\ntry {\n\n $msixPath = $env:INSTALLER_PATH\n if (-not $msixPath) {\n throw \"INSTALLER_PATH is not set\"\n }\n\n Write-Host \"Provisioning MSIX for all users...\"\n $result = Add-AppxProvisionedPackage -Online -PackagePath $msixPath -SkipLicense -Regions \"all\" -ErrorAction Stop\n $result | Out-String | Write-Host\n\n # Win32_ComputerSystem.UserName returns the console user (DOMAIN\\User) or null when no\n # interactive session is active. Other RDP/fast-user-switch sessions won't get the\n # immediate registration; those users will pick it up from the provisioned install at\n # their next sign-in.\n $userName = (Get-CimInstance Win32_ComputerSystem).UserName\n if (-not $userName -or $userName -notlike \"*\\*\") {\n Write-Host \"No interactive user logged on; provisioned install will register for each user at sign-in.\"\n Start-Sleep -Seconds 5\n Exit 0\n }\n\n Write-Host \"Registering MSIX for logged-on user '$userName' via scheduled task...\"\n\n $userScript = @\"\n`$msixPath = \"$msixPath\"\n`$exitCodeFile = \"$exitCodeFile\"\ntry {\n Add-AppxPackage -Path `$msixPath -ErrorAction Stop | Out-String | Write-Host\n Set-Content -Path `$exitCodeFile -Value 0\n} catch {\n Write-Host \"Add-AppxPackage failed: `$(`$_.Exception.Message)\"\n Set-Content -Path `$exitCodeFile -Value 1\n}\n\"@\n\n Set-Content -Path $scriptPath -Value $userScript -Force\n\n $action = New-ScheduledTaskAction -Execute \"powershell.exe\" `\n -Argument \"-WindowStyle Hidden -ExecutionPolicy Bypass -File `\"$scriptPath`\"\"\n $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n $principal = New-ScheduledTaskPrincipal -UserId $userName -RunLevel Highest\n $task = New-ScheduledTask -Action $action -Settings $settings -Principal $principal\n Register-ScheduledTask -TaskName $taskName -InputObject $task -User $userName -Force | Out-Null\n Start-ScheduledTask -TaskName $taskName\n\n $startDate = Get-Date\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n while ($state -ne \"Running\") {\n Start-Sleep -Seconds 1\n if ((New-Timespan -Start $startDate).TotalSeconds -gt 30) {\n Write-Host \"Per-user registration task did not start within 30s; provisioned install is still valid.\"\n break\n }\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n }\n\n while ($state -eq \"Running\") {\n Start-Sleep -Seconds 2\n if ((New-Timespan -Start $startDate).TotalSeconds -gt 90) {\n Write-Host \"Per-user registration task did not complete within 90s; provisioned install is still valid.\"\n break\n }\n $state = (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue).State\n }\n\n if (Test-Path $exitCodeFile) {\n $code = (Get-Content $exitCodeFile -ErrorAction SilentlyContinue | Select-Object -First 1).Trim()\n if ($code -eq \"0\") {\n Write-Host \"Per-user registration completed for '$userName'.\"\n } else {\n Write-Host \"Per-user registration did not complete cleanly (exit code: $code). Provisioned install is still valid.\"\n }\n }\n\n Start-Sleep -Seconds 5\n Exit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n} finally {\n Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null\n Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue\n Remove-Item -Path $exitCodeFile -Force -ErrorAction SilentlyContinue\n}\n", + "f0d0fed1": "$timeoutSeconds = 300 # 5 minute timeout\n\n# Match only the Nightly channel: its MSIX identity \"Mozilla.MozillaFirefoxNightly\"\n# cannot collide with other Firefox channels' identities. Don't match on a\n# PackageFamilyName property: Get-AppxProvisionedPackage doesn't expose it, so an\n# \"-eq\" match is $null for every package.\nfunction ShouldRemoveFirefoxNightlyPackage {\n param([Parameter(Mandatory=$true)]$pkg)\n try {\n $name = [string]$pkg.Name\n $family = [string]$pkg.PackageFamilyName\n\n if ($name -and ($name -like \"*MozillaFirefoxNightly*\")) { return $true }\n if ($family -and ($family -like \"*MozillaFirefoxNightly*\")) { return $true }\n } catch {}\n return $false\n}\n\ntry {\n\n $start = Get-Date\n\n $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object {\n ($_.DisplayName -and ($_.DisplayName -like \"*MozillaFirefoxNightly*\")) -or\n ($_.PackageName -and ($_.PackageName -like \"*MozillaFirefoxNightly*\"))\n }\n foreach ($pkg in $provisioned) {\n Write-Host \"Removing provisioned package: $($pkg.PackageName)\"\n Remove-AppxProvisionedPackage -Online -PackageName $pkg.PackageName -AllUsers -ErrorAction Stop | Out-String | Write-Host\n $elapsed = (New-TimeSpan -Start $start).TotalSeconds\n if ($elapsed -gt $timeoutSeconds) {\n Exit 1603\n }\n }\n\n $installed = Get-AppxPackage -AllUsers -PackageTypeFilter Main -ErrorAction SilentlyContinue | Where-Object {\n ShouldRemoveFirefoxNightlyPackage $_\n }\n foreach ($app in $installed) {\n Write-Host \"Removing installed package: $($app.PackageFullName)\"\n Remove-AppxPackage -Package $app.PackageFullName -AllUsers -ErrorAction Stop | Out-String | Write-Host\n $elapsed = (New-TimeSpan -Start $start).TotalSeconds\n if ($elapsed -gt $timeoutSeconds) {\n Exit 1603\n }\n }\n\n Exit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1603\n}\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/FirefoxDeveloperEdition.tsx b/frontend/pages/SoftwarePage/components/icons/FirefoxDeveloperEdition.tsx new file mode 100644 index 0000000000..eaef007243 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/FirefoxDeveloperEdition.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const FirefoxDeveloperEdition = (props: SVGProps) => ( + + + +); +export default FirefoxDeveloperEdition; diff --git a/frontend/pages/SoftwarePage/components/icons/FirefoxNightly.tsx b/frontend/pages/SoftwarePage/components/icons/FirefoxNightly.tsx new file mode 100644 index 0000000000..548e7bc22f --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/FirefoxNightly.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const FirefoxNightly = (props: SVGProps) => ( + + + +); +export default FirefoxNightly; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index b4bbea7c5d..521777be97 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -382,6 +382,8 @@ import Firealpaca from "./Firealpaca"; import FireflyIotaDesktop from "./FireflyIotaDesktop"; import FireflyShimmer from "./FireflyShimmer"; import Firefox from "./Firefox"; +import FirefoxDeveloperEdition from "./FirefoxDeveloperEdition"; +import FirefoxNightly from "./FirefoxNightly"; import Fission from "./Fission"; import FleetDesktop from "./FleetDesktop"; import Flexoptix from "./Flexoptix"; @@ -1508,6 +1510,8 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { firefly: FireflyIotaDesktop, "firefly shimmer": FireflyShimmer, firefox: Firefox, + "firefox developer edition": FirefoxDeveloperEdition, + "firefox nightly": FirefoxNightly, fission: Fission, "fleet desktop": FleetDesktop, "flexoptix app": Flexoptix, @@ -1773,6 +1777,8 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { mos: Mos, "mountain duck": MountainDuck, "mozilla firefox": Firefox, + "mozilla firefox developer edition": FirefoxDeveloperEdition, + "mozilla firefox nightly": FirefoxNightly, "mozilla vpn": MozillaVpn, mqttx: Mqttx, "mullvad browser": MullvadBrowser, diff --git a/website/assets/images/app-icon-firefox@developer-edition-60x60@2x.png b/website/assets/images/app-icon-firefox@developer-edition-60x60@2x.png new file mode 100644 index 0000000000..6d680ce02f Binary files /dev/null and b/website/assets/images/app-icon-firefox@developer-edition-60x60@2x.png differ diff --git a/website/assets/images/app-icon-firefox@nightly-60x60@2x.png b/website/assets/images/app-icon-firefox@nightly-60x60@2x.png new file mode 100644 index 0000000000..122b0bcdef Binary files /dev/null and b/website/assets/images/app-icon-firefox@nightly-60x60@2x.png differ