Skip to content

Implement onDeterminingFilename#316

Closed
ssm9 wants to merge 19 commits into
mozilla-firefox:mainfrom
ssm9:ssm9/onDeterminingFilename
Closed

Implement onDeterminingFilename#316
ssm9 wants to merge 19 commits into
mozilla-firefox:mainfrom
ssm9:ssm9/onDeterminingFilename

Conversation

@ssm9

@ssm9 ssm9 commented Jul 3, 2026

Copy link
Copy Markdown

A first pass implementation of chrome.downloads.onDeterminingFilename, as reported in https://bugzilla.mozilla.org/show_bug.cgi?id=1245652

Tested out with https://github.com/unintended/download-organizer-chrome-extension with minimal changes for Firefox compatibility.

Happy to iterate on this :)

ssm9 added 19 commits July 2, 2026 20:27
Add the onDeterminingFilename event definition to the downloads API
schema. Listeners receive a DownloadItem and may return an optional
{ filename, conflictAction } object to influence the final filename
before the download begins.
Add _determineFilenameCallbacks set and determineFilename() method to
DownloadIntegration. This provides a registration point for the
WebExtensions downloads API to intercept filename determination for all
downloads before execution begins.
…tart()

Make Download.start() async and call DownloadIntegration.determineFilename()
before execution begins. This is the single interception point that covers
all downloads — both extension-initiated and browser-initiated (legacy) —
since start() is always called before the target path is locked in by the
saver.
Replace the ignoreEvent() stub with a real EventManager implementation.
Listeners register a callback with DownloadIntegration._determineFilenameCallbacks
that fires fire.async(item.serialize()) for each download before it starts.
The first listener to return a { filename, conflictAction } suggestion wins;
the suggestion is validated (no absolute paths, no path traversal) and
applied to download.target.path before execution begins.

Covers both extension-initiated downloads (via downloads.download()) and
all other browser-initiated downloads via the DownloadCore.start() hook.
… test

onDeterminingFilename is now a real event, not an empty event handler,
so remove the comment that said otherwise.
Test cases cover:
- Event fires with correct DownloadItem properties
- Listener can rename the downloaded file
- Listener can place the file in a subdirectory
- conflictAction "uniquify" creates a unique filename when target exists
- conflictAction "overwrite" replaces an existing file
- Absolute path suggestions are silently rejected
- Path traversal (..) suggestions are silently rejected
- First listener's non-null suggestion wins when multiple are registered
- removeListener stops the callback from firing
- Download proceeds normally when no listener is registered
Move onDeterminingFilename from an ad-hoc EventManager with an explicit
register function into PERSISTENT_EVENTS so that the listener survives
event-page suspension.  Also stop iterating callbacks after the first
one that successfully changes the target path (first suggestion wins).
Log each determineFilename() call and each callback invocation for
debugging.  Stop iterating after the first callback that changes the
download's target path so that the first listener's suggestion wins.
Chrome's onDeterminingFilename API passes a suggest() callback as the
second argument to listeners.  Functions cannot cross the IPC boundary,
so inject suggest() in the child process by wrapping each listener at
addListener() time.  Handles both the synchronous call pattern and the
async pattern where the listener returns true and calls suggest() later.
Adds determineFilenameBeforeDialog() to fire onDeterminingFilename
listeners before the auto-save path or Save As dialog is chosen.
The _determinedDownloads WeakSet prevents determineFilename() in
Download.start() from firing the event a second time for the same
download. Also adds _processedLaunchers to track which
nsIHelperAppLauncher objects have already been through HelperAppDlg,
so DownloadLegacy can set the guard when creating the Download object.
When HelperAppDlg fires onDeterminingFilename before showing the Save
As dialog, mark the nsIHelperAppLauncher as processed. DownloadLegacy
checks this flag when the Download object is created and calls
markFilenameAlreadyDetermined() so Download.start() skips the event.
…oads.download()

When saveAs is true, fire onDeterminingFilename listeners on a synthetic
download before showing the file picker, so extensions can redirect the
filename before the user sees the dialog. Uses _syntheticSerialized to
avoid adding the download to DownloadMap prematurely. Marks the download
as already determined so Download.start() does not re-fire the event.
…h is chosen

Call determineFilenameBeforeDialog() at the start of
promptForSaveToFileAsync() so onDeterminingFilename listeners can
redirect the download before auto-save or the Save As file picker
runs. For auto-save, create any suggested subdirectory and pass only
the leaf name to validateLeafName(). For the file picker, open the
picker in the suggested subdirectory and show only the leaf name as
the default string, skipping the lastDir override when a subdirectory
was explicitly suggested by an extension.
…pplyFilenameSuggestion

createNiceUniqueFile physically creates a zero-byte file to claim the
name, which is correct when a download is about to start but wrong
when called from applyFilenameSuggestion — the file picker hasn't run
yet, so the placeholder appears in the directory and triggers a
"file already exists" prompt when the user tries to save.

Inline the same base(N)ext loop using splitBaseNameAndExtension to
find a non-conflicting name without touching the filesystem. The
actual placeholder is created later by validateLeafName (auto-save
path) or the download core (file picker path).
…vior

- test_onDeterminingFilename_double_fire_guard: verifies that
  markFilenameAlreadyDetermined() prevents the listener from firing in
  Download.start() when the event was already dispatched earlier (e.g.
  by HelperAppDlg before the Save As dialog).
- test_onDeterminingFilename_uniquify_increments_counter: verifies that
  when both file.txt and file(1).txt already exist, the suggestion
  resolves to file(2).txt without creating any placeholder file.
- test_onDeterminingFilename_default_conflict_action_is_uniquify:
  verifies that omitting conflictAction defaults to "uniquify" rather
  than silently overwriting the existing file.
@firefoxci-taskcluster

Copy link
Copy Markdown
No Taskcluster jobs started for this pull request

The allowPullRequests configuration for this repository (in .taskcluster.yml on the default branch) does not allow starting tasks for this pull request.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

(Automated Close) Please do not file pull requests here, see https://firefox-source-docs.mozilla.org/contributing/how_to_submit_a_patch.html

@github-actions github-actions Bot closed this Jul 3, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant