-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Implement Custom URL Protocol Handling #15378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
FynnianB
wants to merge
4
commits into
JabRef:main
Choose a base branch
from
FynnianB:feature/protocol-handler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+266
−5
Open
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
docs/decisions/0055-browser-extension-communication-architecture.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| --- | ||
| nav_order: 55 | ||
| parent: Decision Records | ||
| status: proposed | ||
| date: 2026-03-06 | ||
| --- | ||
| # Use Hybrid Architecture (Protocol Handler + HTTP) for Browser Extension Communication | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| JabRef's browser extension imports bibliographic data from web pages into the desktop application via HTTP requests to a local server (`jabsrv`). | ||
| If JabRef is not running, the request fails silently and the import is lost. | ||
| Additionally, the server sets `Access-Control-Allow-Origin: *` without authentication, allowing any website to send requests. | ||
|
|
||
| How should the extension communicate with the desktop application to solve both problems while remaining cross-platform, cross-browser, and maintainable? | ||
|
|
||
| ## Decision Drivers | ||
|
|
||
| * Must work on Windows, macOS, and Linux | ||
| * Must work in Chrome and Firefox under Manifest V3 | ||
| * Must handle the case when JabRef is not running | ||
| * Must authenticate the extension and protect against CSRF | ||
| * Changes must be maintainable by JabRef's open-source community | ||
|
|
||
| ## Considered Options | ||
|
|
||
| * Native Messaging | ||
| * Local HTTP API (status quo) | ||
| * Protocol Handler only | ||
| * Hybrid — Protocol Handler + HTTP | ||
| * WebSocket | ||
| * Companion / Daemon | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| Chosen option: "Hybrid — Protocol Handler + HTTP", because it comes out best (see below). | ||
|
|
||
| The extension sends data via HTTP to `jabsrv` on localhost. | ||
| When JabRef is not running, a `jabref://` protocol handler starts the application; the extension then polls until the HTTP endpoint becomes reachable. | ||
|
|
||
| ### Consequences | ||
|
|
||
| * Good, because HTTP is platform- and browser-independent using standard `fetch()` API | ||
| * Good, because the protocol handler starts JabRef when it is not running, without encoding data in the URL | ||
| * Good, because `jabsrv` already provides the HTTP infrastructure | ||
| * Good, because graceful degradation to pure HTTP when handler is not registered | ||
| * Bad, because a localhost listener requires CSRF mitigations (custom headers, origin checks) | ||
| * Bad, because the protocol handler must be registered on three operating systems | ||
| * Bad, because two communication channels increase implementation complexity | ||
|
|
||
| ## Pros and Cons of the Options | ||
|
|
||
| ### Native Messaging | ||
|
|
||
| * Good, because no network listener exposed — best security properties | ||
| * Good, because the browser manages the host process lifecycle | ||
| * Bad, because six manifest variants and two host script languages required | ||
| * Bad, because high packaging overhead across multiple OS and package formats | ||
| * Bad, because JabRef already aims to remove this infrastructure due to maintenance burden (see PR #14884) | ||
|
|
||
| ### Local HTTP API (status quo) | ||
|
|
||
| * Good, because platform- and browser-independent, easy to test | ||
| * Bad, because no mechanism to start JabRef when not running — import is lost | ||
|
|
||
| ### Protocol Handler only | ||
|
|
||
| * Good, because can launch JabRef when not running | ||
| * Bad, because no authentication possible — any website can trigger the URL | ||
| * Bad, because limited URL length | ||
| * Bad, because unidirectional, no response channel | ||
|
|
||
| ### Hybrid (Protocol Handler + HTTP) | ||
|
|
||
| * Good, because HTTP handles data transfer with full response channel | ||
| * Good, because protocol handler carries no data, avoiding the security issues of "Protocol Handler only" | ||
| * Good, because REST endpoints are extensible for future features | ||
| * Bad, because localhost listener requires CSRF mitigations | ||
| * Bad, because two communication channels increase complexity | ||
|
|
||
| ### WebSocket | ||
|
|
||
| * Good, because persistent bidirectional connection with low latency | ||
| * Bad, because no mechanism to start JabRef when not running | ||
| * Bad, because WebSocket is not subject to Same-Origin Policy — any website can connect | ||
|
|
||
| ### Companion / Daemon | ||
|
|
||
| * Good, because handles the offline-app case via local queue | ||
| * Bad, because three fundamentally different service managers per OS | ||
| * Bad, because effectively a second software project with own build system and CI/CD | ||
|
|
||
| ## More Information | ||
|
|
||
| * [Issue #17: Architecture discussion](https://github.com/JabRef/JabRef-Browser-Extension-fresh/issues/17) | ||
| * [PR #18: Protocol Handler PoC](https://github.com/JabRef/JabRef-Browser-Extension-fresh/pull/18) | ||
| * [PR #14884: Remove Native Messaging infrastructure](https://github.com/JabRef/jabref/pull/14884) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
jabgui/src/test/java/org/jabref/cli/ArgumentProcessorTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package org.jabref.cli; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.jabref.gui.preferences.GuiPreferences; | ||
| import org.jabref.logic.UiCommand; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.mockito.Mockito.mock; | ||
|
|
||
| class ArgumentProcessorTest { | ||
|
|
||
| private final GuiPreferences preferences = mock(GuiPreferences.class); | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {"jabref://", "jabref://open", "jabref:", "jabref://some/path"}) | ||
| void protocolHandlerUrlProducesFocusCommand(String url) { | ||
| ArgumentProcessor processor = new ArgumentProcessor( | ||
| new String[] {url}, | ||
| ArgumentProcessor.Mode.REMOTE_START, | ||
| preferences); | ||
|
|
||
| List<UiCommand> commands = processor.processArguments(); | ||
|
|
||
| assertTrue(commands.stream().anyMatch(UiCommand.Focus.class::isInstance)); | ||
| } | ||
|
|
||
| @Test | ||
| void normalArgumentsAreNotAffectedByProtocolFilter() { | ||
| ArgumentProcessor processor = new ArgumentProcessor( | ||
| new String[] {"--blank"}, | ||
| ArgumentProcessor.Mode.REMOTE_START, | ||
| preferences); | ||
|
|
||
| List<UiCommand> commands = processor.processArguments(); | ||
|
|
||
| assertTrue(commands.stream().anyMatch(UiCommand.BlankWorkspace.class::isInstance)); | ||
| assertFalse(commands.stream().anyMatch(UiCommand.Focus.class::isInstance)); | ||
| } | ||
|
|
||
| @Test | ||
| void protocolHandlerUrlCombinedWithNormalArguments() { | ||
| ArgumentProcessor processor = new ArgumentProcessor( | ||
| new String[] {"jabref://", "--blank"}, | ||
| ArgumentProcessor.Mode.REMOTE_START, | ||
| preferences); | ||
|
|
||
| List<UiCommand> commands = processor.processArguments(); | ||
|
|
||
| assertTrue(commands.stream().anyMatch(UiCommand.BlankWorkspace.class::isInstance)); | ||
| } | ||
|
|
||
| @Test | ||
| void emptyArgumentsProduceNoFocusCommand() { | ||
| ArgumentProcessor processor = new ArgumentProcessor( | ||
| new String[] {}, | ||
| ArgumentProcessor.Mode.REMOTE_START, | ||
| preferences); | ||
|
|
||
| List<UiCommand> commands = processor.processArguments(); | ||
|
|
||
| assertFalse(commands.stream().anyMatch(UiCommand.Focus.class::isInstance)); | ||
| } | ||
|
|
||
| @Test | ||
| void onlyProtocolHandlerUrlProducesOnlyFocusCommand() { | ||
| ArgumentProcessor processor = new ArgumentProcessor( | ||
| new String[] {"jabref://"}, | ||
| ArgumentProcessor.Mode.REMOTE_START, | ||
| preferences); | ||
|
|
||
| List<UiCommand> commands = processor.processArguments(); | ||
|
|
||
| assertEquals(1, commands.size()); | ||
| assertTrue(commands.getFirst() instanceof UiCommand.Focus); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.