Repository: qutebrowser/qutebrowser

Issue:
'Your internet access is blocked' when using keyboard 'f' to follow a link in a local file to a remote one, but works if clicking with mouse
**Version info**:
<!-- Please copy the first block from :version, not just the qutebrowser version -->
qutebrowser v3.0.0
Git commit: 
Backend: QtWebEngine 6.5.2, based on Chromium 108.0.5359.220 (from api)
Qt: 6.5.2

**Does the bug happen if you start with `--temp-basedir`?**:
Yes.

**Description**
I have a bookmarks.html file with commonly-used links.  I load the file into qutebrowser with ':open -t file:///home/chris/ckstemp.html'.  If I use the keyboard shortcut 'f' to follow a link, then press the link letter (say, 'a'), I get a 'Your internet access is blocked' error page.  If instead I click the link with the mouse, the page loads as expected (no error).

I have tried deleting the .cache, and I've tried clearing my browser history.

This is new behavior since I upgraded to qutebrowser 3.0.0 this morning.

**How to reproduce**
<!-- Link to the affected site, or steps to reproduce the issue (if possible/applicable). -->

See above for the steps to reproduces.  Here is the contents of my ckstemp.html file:

```
<TITLE>Bookmarks</TITLE>
<DL><p>
    <DT><H1>Bookmarks</H1>
        <DT><H3>Comics</H3>
        <DL><p>
            <DT><A HREF="https://www.gocomics.com/foxtrot">Foxtrot</A>
         </DL>
</DL>
```




Discussion:
This is due to hints using JS by default to navigate to the link, and Qt 6 being stricter about navigations from local origins to remote ones.

There are three workarounds you could do:

- Host the page somewhere
- Use `qute://bookmarks` and qutebrowser's bookmark functionality instead
- Set `content.local_content_can_access_remote_urls` to true, with the caveat that locally opened websites will be able to access all remote content.

As for a fix for this, I suppose we could either:

- Let the user specify that an user gesture should be forced when doing `:hint` with some flag, similar to `--force-event` for `:click-element`
- Force an user gesture for all hints on `file://` pages, similar to what we do already for `qute://` pages since 8defe1ae44c1c524e937ae08ed16052ee0724e0f

Resolving patch:
diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py
--- a/qutebrowser/browser/webengine/webengineelem.py
+++ b/qutebrowser/browser/webengine/webengineelem.py
@@ -215,14 +215,16 @@ def _requires_user_interaction(self) -> bool:
             return False
 
         # Qt 6.3+ needs a user interaction to allow navigations from qute:// to
-        # outside qute:// (like e.g. on qute://bookmarks).
+        # outside qute:// (like e.g. on qute://bookmarks), as well as from file:// to
+        # outside of file:// (e.g. users having a local bookmarks.html).
         versions = version.qtwebengine_versions()
-        if (
-            baseurl.scheme() == "qute" and
-            url.scheme() != "qute" and
-            versions.webengine >= utils.VersionNumber(6, 3)
-        ):
-            return True
+        for scheme in ["qute", "file"]:
+            if (
+                baseurl.scheme() == scheme and
+                url.scheme() != scheme and
+                versions.webengine >= utils.VersionNumber(6, 3)
+            ):
+                return True
 
         return url.scheme() not in urlutils.WEBENGINE_SCHEMES
 


Test patch:
diff --git a/tests/end2end/data/hints/link_inject.html b/tests/end2end/data/hints/link_inject.html
new file mode 100644
--- /dev/null
+++ b/tests/end2end/data/hints/link_inject.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>A link to use hints on</title>
+        <script>
+            function injectPort() {
+                const queryString = document.location.search;
+                const params = new URLSearchParams(queryString);
+                const port = params.get("port")
+                let link = document.getElementById("link");
+                link.href = link.href.replace("<port>", port);
+            }
+        </script>
+    </head>
+    <body onload="injectPort()">
+        <a href="http://localhost:<port>/data/hello.txt" id="link">Follow me!</a>
+    </body>
+</html>
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -33,8 +33,7 @@ def _get_echo_exe_path():
         Path to the "echo"-utility.
     """
     if utils.is_windows:
-        return os.path.join(testutils.abs_datapath(), 'userscripts',
-                            'echo.bat')
+        return str(testutils.abs_datapath() / 'userscripts' / 'echo.bat')
     else:
         return shutil.which("echo")
 
@@ -199,6 +198,7 @@ def open_path(quteproc, server, path):
     - With "... as a URL", it's opened according to new_instance_open_target.
     """
     path = path.replace('(port)', str(server.port))
+    path = testutils.substitute_testdata(path)
 
     new_tab = False
     new_bg_tab = False
@@ -270,7 +270,7 @@ def run_command(quteproc, server, tmpdir, command):
         invalid = False
 
     command = command.replace('(port)', str(server.port))
-    command = command.replace('(testdata)', testutils.abs_datapath())
+    command = testutils.substitute_testdata(command)
     command = command.replace('(tmpdir)', str(tmpdir))
     command = command.replace('(dirsep)', os.sep)
     command = command.replace('(echo-exe)', _get_echo_exe_path())
@@ -364,7 +364,7 @@ def hint(quteproc, args):
 
 @bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}'))
 def hint_and_follow(quteproc, args, letter):
-    args = args.replace('(testdata)', testutils.abs_datapath())
+    args = testutils.substitute_testdata(args)
     args = args.replace('(python-executable)', sys.executable)
     quteproc.send_cmd(':hint {}'.format(args))
     quteproc.wait_for(message='hints: *')
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -44,6 +44,13 @@ Feature: Using hints
             - data/hints/link_blank.html
             - data/hello.txt
 
+    # https://github.com/qutebrowser/qutebrowser/issues/7842
+    @qtwebkit_skip
+    Scenario: Following a hint from a local file to a remote origin
+        When I open file://(testdata)/hints/link_inject.html?port=(port)
+        And I hint with args "links" and follow a
+        Then data/hello.txt should be loaded
+
     Scenario: Following a hint to link with sub-element and force to open in current tab.
         When I open data/hints/link_span.html
         And I hint with args "links current" and follow a
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -393,7 +393,7 @@ def path_to_url(self, path, *, port=None, https=False):
         verbatim.
         """
         special_schemes = ['about:', 'qute:', 'chrome:', 'view-source:',
-                           'data:', 'http:', 'https:']
+                           'data:', 'http:', 'https:', 'file:']
         server = self.request.getfixturevalue('server')
         server_port = server.port if port is None else port
 
diff --git a/tests/helpers/testutils.py b/tests/helpers/testutils.py
--- a/tests/helpers/testutils.py
+++ b/tests/helpers/testutils.py
@@ -192,8 +192,26 @@ def pattern_match(*, pattern, value):
 
 def abs_datapath():
     """Get the absolute path to the end2end data directory."""
-    file_abs = os.path.abspath(os.path.dirname(__file__))
-    return os.path.join(file_abs, '..', 'end2end', 'data')
+    path = pathlib.Path(__file__).parent / '..' / 'end2end' / 'data'
+    return path.resolve()
+
+
+def substitute_testdata(path):
+    r"""Replace the (testdata) placeholder in path with `abs_datapath()`.
+
+    If path is starting with file://, return path as an URI with file:// removed. This
+    is useful if path is going to be inserted into an URI:
+
+    >>> path = substitute_testdata("C:\Users\qute")
+    >>> f"file://{path}/slug  # results in valid URI
+    'file:///C:/Users/qute/slug'
+    """
+    if path.startswith('file://'):
+        testdata_path = abs_datapath().as_uri().replace('file://', '')
+    else:
+        testdata_path = str(abs_datapath())
+
+    return path.replace('(testdata)', testdata_path)
 
 
 @contextlib.contextmanager