Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import io
import os
import re
import shlex
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
Expand Down Expand Up @@ -55,6 +57,14 @@ def _test(self, meth, *, args=[URL], kw={}, options, arguments):
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):

Expand Down Expand Up @@ -109,6 +119,15 @@ def test_open_bad_new_parameter(self):
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):

Expand Down Expand Up @@ -301,6 +320,81 @@ def test_open_new_tab(self):
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):
Expand Down
67 changes: 60 additions & 7 deletions Lib/webbrowser.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#! /usr/bin/env python3
"""Interfaces for launching and remotely controlling web browsers."""
# Maintained by Georg Brandl.

Expand Down Expand Up @@ -164,6 +163,12 @@ def open_new(self, url):
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
Expand All @@ -181,6 +186,7 @@ def __init__(self, name):

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:
Expand All @@ -201,6 +207,7 @@ def open(self, url, new=0, autoraise=True):
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)
Expand Down Expand Up @@ -280,7 +287,9 @@ def open(self, url, new=0, autoraise=True):
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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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:
Expand All @@ -576,6 +599,7 @@ def register_standard_browsers():
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:
Expand All @@ -596,9 +620,35 @@ def __init__(self, name='default'):

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}"
Expand All @@ -607,7 +657,7 @@ def open(self, url, new=0, autoraise=True):
end
'''

osapipe = os.popen("osascript", "w")
osapipe = os.popen("/usr/bin/osascript", "w")
if osapipe is None:
return False

Expand All @@ -627,6 +677,7 @@ def open(self, url, new=0, autoraise=True):
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
Expand Down Expand Up @@ -682,7 +733,9 @@ def open(self, url, new=0, autoraise=True):

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()
Expand Down
Loading