From f8e0eeb579c0ca2d8bf607822f2852226bd6db57 Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Wed, 13 May 2026 13:53:31 +0300 Subject: [PATCH] Update `webbrowser.py` to 3.14.5 (#7868) --- Lib/test/test_webbrowser.py | 94 +++++++++++++++++++++++++++++++++++++ Lib/webbrowser.py | 67 +++++++++++++++++++++++--- 2 files changed, 154 insertions(+), 7 deletions(-) mode change 100755 => 100644 Lib/webbrowser.py diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 4fcbc5c2e..bfbcf112b 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -1,3 +1,4 @@ +import io import os import re import shlex @@ -5,6 +6,7 @@ import subprocess import sys import unittest import webbrowser +from functools import partial from test import support from test.support import import_helper from test.support import is_apple_mobile @@ -55,6 +57,14 @@ class CommandTestMixin: popen_args.pop(popen_args.index(option)) self.assertEqual(popen_args, arguments) + def test_reject_dash_prefixes(self): + browser = self.browser_class(name=CMD_NAME) + with self.assertRaisesRegex( + ValueError, + r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$" + ): + browser.open(f"--key=val {URL}") + class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase): @@ -109,6 +119,15 @@ class ChromeCommandTest(CommandTestMixin, unittest.TestCase): arguments=[URL], kw=dict(new=999)) + def test_reject_action_dash_prefixes(self): + browser = self.browser_class(name=CMD_NAME) + with self.assertRaises(ValueError): + browser.open('%action--incognito') + # new=1: action is "--new-window", so "%action" itself expands to + # a dash-prefixed flag even with no dash in the original URL. + with self.assertRaises(ValueError): + browser.open('%action', new=1) + class EdgeCommandTest(CommandTestMixin, unittest.TestCase): @@ -301,6 +320,81 @@ class IOSBrowserTest(unittest.TestCase): self._test('open_new_tab') +class MockPopenPipe: + def __init__(self, cmd, mode): + self.cmd = cmd + self.mode = mode + self.pipe = io.StringIO() + self._closed = False + + def write(self, buf): + self.pipe.write(buf) + + def close(self): + self._closed = True + return None + + +@unittest.skipUnless(sys.platform == "darwin", "macOS specific test") +@requires_subprocess() +class MacOSXOSAScriptTest(unittest.TestCase): + def setUp(self): + # Ensure that 'BROWSER' is not set to 'open' or something else. + # See: https://github.com/python/cpython/issues/131254. + env = self.enterContext(os_helper.EnvironmentVarGuard()) + env.unset("BROWSER") + + support.patch(self, os, "popen", self.mock_popen) + self.browser = webbrowser.MacOSXOSAScript("default") + + def mock_popen(self, cmd, mode): + self.popen_pipe = MockPopenPipe(cmd, mode) + return self.popen_pipe + + def test_default(self): + browser = webbrowser.get() + assert isinstance(browser, webbrowser.MacOSXOSAScript) + self.assertEqual(browser.name, "default") + + def test_default_open(self): + url = "https://python.org" + self.browser.open(url) + self.assertTrue(self.popen_pipe._closed) + self.assertEqual(self.popen_pipe.cmd, "/usr/bin/osascript") + script = self.popen_pipe.pipe.getvalue() + self.assertEqual(script.strip(), f'open location "{url}"') + + def test_url_quote(self): + self.browser.open('https://python.org/"quote"') + script = self.popen_pipe.pipe.getvalue() + self.assertEqual( + script.strip(), 'open location "https://python.org/%22quote%22"' + ) + + def test_default_browser_lookup(self): + url = "file:///tmp/some-file.html" + self.browser.open(url) + script = self.popen_pipe.pipe.getvalue() + # doesn't actually test the browser lookup works, + # just that the branch is taken + self.assertIn("URLForApplicationToOpenURL", script) + self.assertIn(f'open location "{url}"', script) + + def test_explicit_browser(self): + browser = webbrowser.MacOSXOSAScript("safari") + browser.open("https://python.org") + script = self.popen_pipe.pipe.getvalue() + self.assertIn('tell application "safari"', script) + self.assertIn('open location "https://python.org"', script) + + def test_reject_dash_prefixes(self): + with self.assertRaisesRegex( + ValueError, + r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$" + ): + self.browser.open(f"--key=val {URL}") + + class BrowserRegistrationTest(unittest.TestCase): def setUp(self): diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py old mode 100755 new mode 100644 index 2f9555ad6..97aad6eea --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -1,4 +1,3 @@ -#! /usr/bin/env python3 """Interfaces for launching and remotely controlling web browsers.""" # Maintained by Georg Brandl. @@ -164,6 +163,12 @@ class BaseBrowser: def open_new_tab(self, url): return self.open(url, 2) + @staticmethod + def _check_url(url): + """Ensures that the URL is safe to pass to subprocesses as a parameter""" + if url and url.lstrip().startswith("-"): + raise ValueError(f"Invalid URL (leading dash disallowed): {url!r}") + class GenericBrowser(BaseBrowser): """Class for all browsers started with a command @@ -181,6 +186,7 @@ class GenericBrowser(BaseBrowser): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + self._check_url(url) cmdline = [self.name] + [arg.replace("%s", url) for arg in self.args] try: @@ -201,6 +207,7 @@ class BackgroundBrowser(GenericBrowser): cmdline = [self.name] + [arg.replace("%s", url) for arg in self.args] sys.audit("webbrowser.open", url) + self._check_url(url) try: if sys.platform[:3] == 'win': p = subprocess.Popen(cmdline) @@ -280,7 +287,9 @@ class UnixBrowser(BaseBrowser): raise Error("Bad 'new' parameter to open(); " f"expected 0, 1, or 2, got {new}") - args = [arg.replace("%s", url).replace("%action", action) + self._check_url(url.replace("%action", action)) + + args = [arg.replace("%action", action).replace("%s", url) for arg in self.remote_args] args = [arg for arg in args if arg] success = self._invoke(args, True, autoraise, url) @@ -358,6 +367,7 @@ class Konqueror(BaseBrowser): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + self._check_url(url) # XXX Currently I know no way to prevent KFM from opening a new win. if new == 2: action = "newTab" @@ -483,10 +493,10 @@ def register_standard_browsers(): if sys.platform == 'darwin': register("MacOSX", None, MacOSXOSAScript('default')) - register("chrome", None, MacOSXOSAScript('chrome')) + register("chrome", None, MacOSXOSAScript('google chrome')) register("firefox", None, MacOSXOSAScript('firefox')) register("safari", None, MacOSXOSAScript('safari')) - # OS X can use below Unix support (but we prefer using the OS X + # macOS can use below Unix support (but we prefer using the macOS # specific stuff) if sys.platform == "ios": @@ -560,6 +570,19 @@ def register_standard_browsers(): # Treat choices in same way as if passed into get() but do register # and prepend to _tryorder for cmdline in userchoices: + if all(x not in cmdline for x in " \t"): + # Assume this is the name of a registered command, use + # that unless it is a GenericBrowser. + try: + command = _browsers[cmdline.lower()] + except KeyError: + pass + + else: + if not isinstance(command[1], GenericBrowser): + _tryorder.insert(0, cmdline.lower()) + continue + if cmdline != '': cmd = _synthesize(cmdline, preferred=True) if cmd[1] is None: @@ -576,6 +599,7 @@ if sys.platform[:3] == "win": class WindowsDefault(BaseBrowser): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + self._check_url(url) try: os.startfile(url) except OSError: @@ -596,9 +620,35 @@ if sys.platform == 'darwin': def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + self._check_url(url) url = url.replace('"', '%22') if self.name == 'default': - script = f'open location "{url}"' # opens in default browser + proto, _sep, _rest = url.partition(":") + if _sep and proto.lower() in {"http", "https"}: + # default web URL, don't need to lookup browser + script = f'open location "{url}"' + else: + # if not a web URL, need to lookup default browser to ensure a browser is launched + # this should always work, but is overkill to lookup http handler + # before launching http + script = f""" + use framework "AppKit" + use AppleScript version "2.4" + use scripting additions + + property NSWorkspace : a reference to current application's NSWorkspace + property NSURL : a reference to current application's NSURL + + set http_url to NSURL's URLWithString:"https://python.org" + set browser_url to (NSWorkspace's sharedWorkspace)'s ¬ + URLForApplicationToOpenURL:http_url + set app_path to browser_url's relativePath as text -- NSURL to absolute path '/Applications/Safari.app' + + tell application app_path + activate + open location "{url}" + end tell + """ else: script = f''' tell application "{self.name}" @@ -607,7 +657,7 @@ if sys.platform == 'darwin': end ''' - osapipe = os.popen("osascript", "w") + osapipe = os.popen("/usr/bin/osascript", "w") if osapipe is None: return False @@ -627,6 +677,7 @@ if sys.platform == "ios": class IOSBrowser(BaseBrowser): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + self._check_url(url) # If ctypes isn't available, we can't open a browser if objc is None: return False @@ -682,7 +733,9 @@ if sys.platform == "ios": def parse_args(arg_list: list[str] | None): import argparse - parser = argparse.ArgumentParser(description="Open URL in a web browser.") + parser = argparse.ArgumentParser( + description="Open URL in a web browser.", color=True, + ) parser.add_argument("url", help="URL to open") group = parser.add_mutually_exclusive_group()