mirror of
https://github.com/python/cpython.git
synced 2026-05-04 09:31:02 +00:00
Merge 3b390bb7bb into 68fe899feb
This commit is contained in:
commit
ed3aa580bb
7 changed files with 214 additions and 15 deletions
|
|
@ -37,6 +37,11 @@ Pending removal in Python 3.17
|
|||
is deprecated and scheduled for removal in Python 3.17.
|
||||
(Contributed by Stan Ulbrych in :gh:`136702`.)
|
||||
|
||||
* :mod:`webbrowser`:
|
||||
|
||||
- :class:`!webbrowser.MacOSXOSAScript` is deprecated in favour of
|
||||
:class:`!webbrowser.MacOS`. (:gh:`137586`)
|
||||
|
||||
* :mod:`typing`:
|
||||
|
||||
- Before Python 3.14, old-style unions were implemented using the private class
|
||||
|
|
|
|||
|
|
@ -172,14 +172,16 @@ for the controller classes, all defined in this module.
|
|||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'windows-default'`` | ``WindowsDefault`` | \(2) |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'macosx'`` | ``MacOSXOSAScript('default')`` | \(3) |
|
||||
| ``'macos'`` | ``MacOS('default')`` | \(3) |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'safari'`` | ``MacOSXOSAScript('safari')`` | \(3) |
|
||||
| ``'safari'`` | ``MacOS('safari')`` | \(3) |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'chrome'`` | ``MacOS('google chrome')`` | \(3) |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'firefox'`` | ``MacOS('firefox')`` | \(3) |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'google-chrome'`` | ``Chrome('google-chrome')`` | |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'chrome'`` | ``Chrome('chrome')`` | |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'chromium'`` | ``Chromium('chromium')`` | |
|
||||
+------------------------+-----------------------------------------+-------+
|
||||
| ``'chromium-browser'`` | ``Chromium('chromium-browser')`` | |
|
||||
|
|
@ -221,6 +223,17 @@ Notes:
|
|||
.. versionchanged:: 3.13
|
||||
Support for iOS has been added.
|
||||
|
||||
.. versionadded:: next
|
||||
:class:`!MacOS` has been added as a replacement for :class:`!MacOSXOSAScript`,
|
||||
opening browsers via :program:`/usr/bin/open` instead of :program:`osascript`.
|
||||
|
||||
.. deprecated-removed:: next 3.17
|
||||
:class:`!MacOSXOSAScript` is deprecated in favour of :class:`!MacOS`.
|
||||
Using :program:`/usr/bin/open` instead of :program:`osascript` is a
|
||||
security and usability improvement: :program:`osascript` may be blocked
|
||||
on managed systems due to its abuse potential as a general-purpose
|
||||
scripting interpreter.
|
||||
|
||||
Here are some simple examples::
|
||||
|
||||
url = 'https://docs.python.org/'
|
||||
|
|
|
|||
|
|
@ -1546,6 +1546,19 @@ wave
|
|||
(Contributed by Lionel Koenig and Michiel W. Beijen in :gh:`60729`.)
|
||||
|
||||
|
||||
webbrowser
|
||||
----------
|
||||
|
||||
* On macOS, the new :class:`!webbrowser.MacOS` class opens URLs via
|
||||
:program:`/usr/bin/open` instead of constructing and executing AppleScript
|
||||
via :program:`osascript`. The default browser is detected from the
|
||||
LaunchServices preferences file using :mod:`plistlib`, with
|
||||
:class:`!com.apple.Safari` as the fallback on fresh installations.
|
||||
For non-HTTP(S) URLs, :program:`open -b <bundle-id>` is used to route the
|
||||
URL through a browser rather than the OS file handler, preventing
|
||||
file injection attacks.
|
||||
(Contributed by Jeff Lyon in :gh:`137586`.)
|
||||
|
||||
xml.parsers.expat
|
||||
-----------------
|
||||
|
||||
|
|
@ -1961,6 +1974,12 @@ New deprecations
|
|||
merely imported or accessed from the :mod:`!typing` module.
|
||||
|
||||
|
||||
* :mod:`webbrowser`:
|
||||
|
||||
* :class:`!webbrowser.MacOSXOSAScript` is deprecated in favour of
|
||||
:class:`!webbrowser.MacOS` and scheduled for removal in Python 3.17.
|
||||
(Contributed by Jeff Lyon in :gh:`137586`.)
|
||||
|
||||
* ``__version__``
|
||||
|
||||
* The ``__version__``, ``version`` and ``VERSION`` attributes have been
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
import warnings
|
||||
import webbrowser
|
||||
from test import support
|
||||
from test.support import force_not_colorized_test_class
|
||||
|
|
@ -335,6 +336,83 @@ def close(self):
|
|||
return None
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "macOS specific test")
|
||||
@requires_subprocess()
|
||||
class MacOSTest(unittest.TestCase):
|
||||
|
||||
def test_default(self):
|
||||
browser = webbrowser.get()
|
||||
self.assertIsInstance(browser, webbrowser.MacOS)
|
||||
self.assertEqual(browser.name, 'default')
|
||||
|
||||
def test_default_http_open(self):
|
||||
# http/https URLs use /usr/bin/open directly — no bundle ID needed.
|
||||
browser = webbrowser.MacOS('default')
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0)
|
||||
result = browser.open(URL)
|
||||
mock_run.assert_called_once_with(
|
||||
['/usr/bin/open', URL],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_default_non_http_uses_bundle_id(self):
|
||||
# Non-http(s) URLs (e.g. file://) must be routed through the browser
|
||||
# via -b <bundle-id> to prevent OS file handler dispatch.
|
||||
file_url = 'file:///tmp/test.html'
|
||||
browser = webbrowser.MacOS('default')
|
||||
with mock.patch('webbrowser._macos_default_browser_bundle_id',
|
||||
return_value='com.google.Chrome'), \
|
||||
mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0)
|
||||
result = browser.open(file_url)
|
||||
mock_run.assert_called_once_with(
|
||||
['/usr/bin/open', '-b', 'com.google.Chrome', file_url],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_named_known_browser_uses_bundle_id(self):
|
||||
# Named browsers with a known bundle ID use /usr/bin/open -b.
|
||||
browser = webbrowser.MacOS('safari')
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0)
|
||||
result = browser.open(URL)
|
||||
mock_run.assert_called_once_with(
|
||||
['/usr/bin/open', '-b', 'com.apple.Safari', URL],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_named_unknown_browser_falls_back_to_dash_a(self):
|
||||
# Named browsers not in the bundle ID map fall back to -a.
|
||||
browser = webbrowser.MacOS('lynx')
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0)
|
||||
browser.open(URL)
|
||||
mock_run.assert_called_once_with(
|
||||
['/usr/bin/open', '-a', 'lynx', URL],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
def test_open_failure(self):
|
||||
browser = webbrowser.MacOS('default')
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=1)
|
||||
result = browser.open(URL)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "macOS specific test")
|
||||
@requires_subprocess()
|
||||
class MacOSXOSAScriptDeprecationTest(unittest.TestCase):
|
||||
|
||||
def test_deprecation_warning(self):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
webbrowser.MacOSXOSAScript('default')
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "macOS specific test")
|
||||
@requires_subprocess()
|
||||
class MacOSXOSAScriptTest(unittest.TestCase):
|
||||
|
|
@ -345,17 +423,14 @@ def setUp(self):
|
|||
env.unset("BROWSER")
|
||||
|
||||
support.patch(self, os, "popen", self.mock_popen)
|
||||
self.enterContext(warnings.catch_warnings())
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
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)
|
||||
|
|
@ -381,7 +456,9 @@ def test_default_browser_lookup(self):
|
|||
self.assertIn(f'open location "{url}"', script)
|
||||
|
||||
def test_explicit_browser(self):
|
||||
browser = webbrowser.MacOSXOSAScript("safari")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
browser = webbrowser.MacOSXOSAScript("safari")
|
||||
browser.open("https://python.org")
|
||||
script = self.popen_pipe.pipe.getvalue()
|
||||
self.assertIn('tell application "safari"', script)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""Interfaces for launching and remotely controlling web browsers."""
|
||||
# Maintained by Georg Brandl.
|
||||
|
||||
import builtins # because we override open
|
||||
import os
|
||||
lazy import plistlib
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
|
|
@ -492,10 +493,15 @@ def register_standard_browsers():
|
|||
_tryorder = []
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
register("MacOSX", None, MacOSXOSAScript('default'))
|
||||
register("chrome", None, MacOSXOSAScript('google chrome'))
|
||||
register("firefox", None, MacOSXOSAScript('firefox'))
|
||||
register("safari", None, MacOSXOSAScript('safari'))
|
||||
register("MacOS", None, MacOS('default'))
|
||||
register("MacOSX", None, MacOS('default')) # backward compat alias
|
||||
register("chrome", None, MacOS('google chrome'))
|
||||
register("chromium", None, MacOS('chromium'))
|
||||
register("firefox", None, MacOS('firefox'))
|
||||
register("safari", None, MacOS('safari'))
|
||||
register("opera", None, MacOS('opera'))
|
||||
register("microsoft-edge", None, MacOS('microsoft edge'))
|
||||
register("brave", None, MacOS('brave browser'))
|
||||
# macOS can use below Unix support (but we prefer using the macOS
|
||||
# specific stuff)
|
||||
|
||||
|
|
@ -614,8 +620,80 @@ def open(self, url, new=0, autoraise=True):
|
|||
#
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
def _macos_default_browser_bundle_id():
|
||||
"""Return the bundle ID of the default web browser.
|
||||
|
||||
Reads the LaunchServices preferences file that macOS maintains
|
||||
when the user sets a default browser. Returns 'com.apple.Safari'
|
||||
if the file is absent or no https handler is recorded, because on
|
||||
a fresh macOS installation Safari is the default browser and the
|
||||
LaunchServices plist is not written until the user explicitly
|
||||
changes their default browser.
|
||||
"""
|
||||
plist = os.path.expanduser(
|
||||
'~/Library/Preferences/com.apple.LaunchServices/'
|
||||
'com.apple.launchservices.secure.plist'
|
||||
)
|
||||
try:
|
||||
with builtins.open(plist, 'rb') as f:
|
||||
data = plistlib.load(f)
|
||||
for handler in data.get('LSHandlers', []):
|
||||
if handler.get('LSHandlerURLScheme') == 'https':
|
||||
return (handler.get('LSHandlerRoleAll')
|
||||
or handler.get('LSHandlerRoleViewer'))
|
||||
except (OSError, KeyError, ValueError):
|
||||
pass
|
||||
return 'com.apple.Safari'
|
||||
|
||||
class MacOS(BaseBrowser):
|
||||
"""Launcher class for macOS browsers, using /usr/bin/open.
|
||||
|
||||
For http/https URLs with the default browser, /usr/bin/open is called
|
||||
directly; macOS routes these to the registered browser.
|
||||
|
||||
For all other URL schemes (e.g. file://) and for named browsers,
|
||||
/usr/bin/open -b <bundle-id> is used so that the URL is always passed
|
||||
to a browser application rather than dispatched by the OS file handler.
|
||||
This prevents file injection attacks where a file:// URL pointing to an
|
||||
executable bundle could otherwise be launched by the OS.
|
||||
|
||||
Named browsers with known bundle IDs use -b; unknown names fall back
|
||||
to -a.
|
||||
"""
|
||||
|
||||
_BUNDLE_IDS = {
|
||||
'google chrome': 'com.google.Chrome',
|
||||
'firefox': 'org.mozilla.firefox',
|
||||
'safari': 'com.apple.Safari',
|
||||
'chromium': 'org.chromium.Chromium',
|
||||
'opera': 'com.operasoftware.Opera',
|
||||
'microsoft edge': 'com.microsoft.edgemac',
|
||||
'brave browser': 'com.brave.Browser',
|
||||
}
|
||||
|
||||
def open(self, url, new=0, autoraise=True):
|
||||
sys.audit("webbrowser.open", url)
|
||||
self._check_url(url)
|
||||
if self.name == 'default':
|
||||
proto, sep, _ = url.partition(':')
|
||||
if sep and proto.lower() in {'http', 'https'}:
|
||||
cmd = ['/usr/bin/open', url]
|
||||
else:
|
||||
bundle_id = _macos_default_browser_bundle_id()
|
||||
cmd = ['/usr/bin/open', '-b', bundle_id, url]
|
||||
else:
|
||||
bundle_id = self._BUNDLE_IDS.get(self.name.lower())
|
||||
if bundle_id:
|
||||
cmd = ['/usr/bin/open', '-b', bundle_id, url]
|
||||
else:
|
||||
cmd = ['/usr/bin/open', '-a', self.name, url]
|
||||
proc = subprocess.run(cmd, stderr=subprocess.DEVNULL)
|
||||
return proc.returncode == 0
|
||||
|
||||
class MacOSXOSAScript(BaseBrowser):
|
||||
def __init__(self, name='default'):
|
||||
import warnings
|
||||
warnings._deprecated("webbrowser.MacOSXOSAScript", remove=(3, 17))
|
||||
super().__init__(name)
|
||||
|
||||
def open(self, url, new=0, autoraise=True):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
Add :class:`!MacOS` to :mod:`webbrowser` for macOS, which opens URLs via
|
||||
``/usr/bin/open`` instead of piping AppleScript to ``osascript``.
|
||||
Deprecate :class:`!MacOSXOSAScript` in favour of :class:`!MacOS`.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Fix a PATH-injection vulnerability in :mod:`webbrowser` on macOS where
|
||||
``osascript`` was invoked without an absolute path. The new :class:`!MacOS`
|
||||
class uses ``/usr/bin/open`` directly, eliminating the dependency on
|
||||
``osascript`` entirely.
|
||||
Loading…
Add table
Add a link
Reference in a new issue