]>
git.ipfire.org Git - thirdparty/Python/cpython.git/blob
3244f206aa
1 #! /usr/bin/env python3
2 """Interfaces for launching and remotely controlling web browsers."""
3 # Maintained by Georg Brandl.
13 __all__
= ["Error", "open", "open_new", "open_new_tab", "get", "register"]
15 class Error(Exception):
18 _lock
= threading
.RLock()
19 _browsers
= {} # Dictionary of available browser controllers
20 _tryorder
= None # Preference order of available browsers
21 _os_preferred_browser
= None # The preferred browser
23 def register(name
, klass
, instance
=None, *, preferred
=False):
24 """Register a browser connector."""
27 register_standard_browsers()
28 _browsers
[name
.lower()] = [klass
, instance
]
30 # Preferred browsers go to the front of the list.
31 # Need to match to the default browser returned by xdg-settings, which
32 # may be of the form e.g. "firefox.desktop".
33 if preferred
or (_os_preferred_browser
and name
in _os_preferred_browser
):
34 _tryorder
.insert(0, name
)
36 _tryorder
.append(name
)
39 """Return a browser launcher instance appropriate for the environment."""
43 register_standard_browsers()
45 alternatives
= [using
]
47 alternatives
= _tryorder
48 for browser
in alternatives
:
50 # User gave us a command line, split it into name and args
51 browser
= shlex
.split(browser
)
52 if browser
[-1] == '&':
53 return BackgroundBrowser(browser
[:-1])
55 return GenericBrowser(browser
)
57 # User gave us a browser name or path.
59 command
= _browsers
[browser
.lower()]
61 command
= _synthesize(browser
)
62 if command
[1] is not None:
64 elif command
[0] is not None:
66 raise Error("could not locate runnable browser")
68 # Please note: the following definition hides a builtin function.
69 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
70 # instead of "from webbrowser import *".
72 def open(url
, new
=0, autoraise
=True):
73 """Display url using the default browser.
75 If possible, open url in a location determined by new.
76 - 0: the same browser window (the default).
77 - 1: a new browser window.
78 - 2: a new browser page ("tab").
79 If possible, autoraise raises the window (the default) or not.
84 register_standard_browsers()
85 for name
in _tryorder
:
87 if browser
.open(url
, new
, autoraise
):
92 """Open url in a new window of the default browser.
94 If not possible, then open url in the only browser window.
98 def open_new_tab(url
):
99 """Open url in a new page ("tab") of the default browser.
101 If not possible, then the behavior becomes equivalent to open_new().
106 def _synthesize(browser
, *, preferred
=False):
107 """Attempt to synthesize a controller based on existing controllers.
109 This is useful to create a controller when a user specifies a path to
110 an entry in the BROWSER environment variable -- we can copy a general
111 controller to operate using a specific installation of the desired
114 If we can't create a controller in this way, or if there is no
115 executable for the requested browser, return [None, None].
118 cmd
= browser
.split()[0]
119 if not shutil
.which(cmd
):
121 name
= os
.path
.basename(cmd
)
123 command
= _browsers
[name
.lower()]
126 # now attempt to clone to fit the new name:
127 controller
= command
[1]
128 if controller
and name
.lower() == controller
.basename
:
130 controller
= copy
.copy(controller
)
131 controller
.name
= browser
132 controller
.basename
= os
.path
.basename(browser
)
133 register(browser
, None, instance
=controller
, preferred
=preferred
)
134 return [None, controller
]
138 # General parent classes
140 class BaseBrowser(object):
141 """Parent class for all browsers. Do not use directly."""
145 def __init__(self
, name
=""):
149 def open(self
, url
, new
=0, autoraise
=True):
150 raise NotImplementedError
152 def open_new(self
, url
):
153 return self
.open(url
, 1)
155 def open_new_tab(self
, url
):
156 return self
.open(url
, 2)
159 class GenericBrowser(BaseBrowser
):
160 """Class for all browsers started with a command
161 and without remote functionality."""
163 def __init__(self
, name
):
164 if isinstance(name
, str):
168 # name should be a list with arguments
171 self
.basename
= os
.path
.basename(self
.name
)
173 def open(self
, url
, new
=0, autoraise
=True):
174 sys
.audit("webbrowser.open", url
)
175 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
176 for arg
in self
.args
]
178 if sys
.platform
[:3] == 'win':
179 p
= subprocess
.Popen(cmdline
)
181 p
= subprocess
.Popen(cmdline
, close_fds
=True)
187 class BackgroundBrowser(GenericBrowser
):
188 """Class for all browsers which are to be started in the
191 def open(self
, url
, new
=0, autoraise
=True):
192 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
193 for arg
in self
.args
]
194 sys
.audit("webbrowser.open", url
)
196 if sys
.platform
[:3] == 'win':
197 p
= subprocess
.Popen(cmdline
)
199 p
= subprocess
.Popen(cmdline
, close_fds
=True,
200 start_new_session
=True)
201 return (p
.poll() is None)
206 class UnixBrowser(BaseBrowser
):
207 """Parent class for all Unix browsers with remote functionality."""
211 redirect_stdout
= True
212 # In remote_args, %s will be replaced with the requested URL. %action will
213 # be replaced depending on the value of 'new' passed to open.
214 # remote_action is used for new=0 (open). If newwin is not None, it is
215 # used for new=1 (open_new). If newtab is not None, it is used for
216 # new=3 (open_new_tab). After both substitutions are made, any empty
217 # strings in the transformed remote_args list will be removed.
218 remote_args
= ['%action', '%s']
220 remote_action_newwin
= None
221 remote_action_newtab
= None
223 def _invoke(self
, args
, remote
, autoraise
, url
=None):
225 if remote
and self
.raise_opts
:
226 # use autoraise argument only for remote invocation
227 autoraise
= int(autoraise
)
228 opt
= self
.raise_opts
[autoraise
]
229 if opt
: raise_opt
= [opt
]
231 cmdline
= [self
.name
] + raise_opt
+ args
233 if remote
or self
.background
:
234 inout
= subprocess
.DEVNULL
236 # for TTY browsers, we need stdin/out
238 p
= subprocess
.Popen(cmdline
, close_fds
=True, stdin
=inout
,
239 stdout
=(self
.redirect_stdout
and inout
or None),
240 stderr
=inout
, start_new_session
=True)
242 # wait at most five seconds. If the subprocess is not finished, the
243 # remote invocation has (hopefully) started a new instance.
246 # if remote call failed, open() will try direct invocation
248 except subprocess
.TimeoutExpired
:
250 elif self
.background
:
258 def open(self
, url
, new
=0, autoraise
=True):
259 sys
.audit("webbrowser.open", url
)
261 action
= self
.remote_action
263 action
= self
.remote_action_newwin
265 if self
.remote_action_newtab
is None:
266 action
= self
.remote_action_newwin
268 action
= self
.remote_action_newtab
270 raise Error("Bad 'new' parameter to open(); " +
271 "expected 0, 1, or 2, got %s" % new
)
273 args
= [arg
.replace("%s", url
).replace("%action", action
)
274 for arg
in self
.remote_args
]
275 args
= [arg
for arg
in args
if arg
]
276 success
= self
._invoke
(args
, True, autoraise
, url
)
278 # remote invocation failed, try straight way
279 args
= [arg
.replace("%s", url
) for arg
in self
.args
]
280 return self
._invoke
(args
, False, False)
285 class Mozilla(UnixBrowser
):
286 """Launcher class for Mozilla browsers."""
288 remote_args
= ['%action', '%s']
290 remote_action_newwin
= "-new-window"
291 remote_action_newtab
= "-new-tab"
295 class Netscape(UnixBrowser
):
296 """Launcher class for Netscape browser."""
298 raise_opts
= ["-noraise", "-raise"]
299 remote_args
= ['-remote', 'openURL(%s%action)']
301 remote_action_newwin
= ",new-window"
302 remote_action_newtab
= ",new-tab"
306 class Galeon(UnixBrowser
):
307 """Launcher class for Galeon/Epiphany browsers."""
309 raise_opts
= ["-noraise", ""]
310 remote_args
= ['%action', '%s']
312 remote_action_newwin
= "-w"
316 class Chrome(UnixBrowser
):
317 "Launcher class for Google Chrome browser."
319 remote_args
= ['%action', '%s']
321 remote_action_newwin
= "--new-window"
322 remote_action_newtab
= ""
328 class Opera(UnixBrowser
):
329 "Launcher class for Opera browser."
331 remote_args
= ['%action', '%s']
333 remote_action_newwin
= "--new-window"
334 remote_action_newtab
= ""
338 class Elinks(UnixBrowser
):
339 "Launcher class for Elinks browsers."
341 remote_args
= ['-remote', 'openURL(%s%action)']
343 remote_action_newwin
= ",new-window"
344 remote_action_newtab
= ",new-tab"
347 # elinks doesn't like its stdout to be redirected -
348 # it uses redirected stdout as a signal to do -dump
349 redirect_stdout
= False
352 class Konqueror(BaseBrowser
):
353 """Controller for the KDE File Manager (kfm, or Konqueror).
355 See the output of ``kfmclient --commands``
356 for more information on the Konqueror remote-control interface.
359 def open(self
, url
, new
=0, autoraise
=True):
360 sys
.audit("webbrowser.open", url
)
361 # XXX Currently I know no way to prevent KFM from opening a new win.
367 devnull
= subprocess
.DEVNULL
370 p
= subprocess
.Popen(["kfmclient", action
, url
],
371 close_fds
=True, stdin
=devnull
,
372 stdout
=devnull
, stderr
=devnull
)
374 # fall through to next variant
378 # kfmclient's return code unfortunately has no meaning as it seems
382 p
= subprocess
.Popen(["konqueror", "--silent", url
],
383 close_fds
=True, stdin
=devnull
,
384 stdout
=devnull
, stderr
=devnull
,
385 start_new_session
=True)
387 # fall through to next variant
391 # Should be running now.
395 p
= subprocess
.Popen(["kfm", "-d", url
],
396 close_fds
=True, stdin
=devnull
,
397 stdout
=devnull
, stderr
=devnull
,
398 start_new_session
=True)
402 return (p
.poll() is None)
405 class Grail(BaseBrowser
):
406 # There should be a way to maintain a connection to Grail, but the
407 # Grail remote control protocol doesn't really allow that at this
408 # point. It probably never will!
409 def _find_grail_rc(self
):
414 tempdir
= os
.path
.join(tempfile
.gettempdir(),
416 user
= pwd
.getpwuid(os
.getuid())[0]
417 filename
= os
.path
.join(glob
.escape(tempdir
), glob
.escape(user
) + "-*")
418 maybes
= glob
.glob(filename
)
421 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
423 # need to PING each one until we find one that's live
427 # no good; attempt to clean it out, but don't fail:
435 def _remote(self
, action
):
436 s
= self
._find
_grail
_rc
()
443 def open(self
, url
, new
=0, autoraise
=True):
444 sys
.audit("webbrowser.open", url
)
446 ok
= self
._remote
("LOADNEW " + url
)
448 ok
= self
._remote
("LOAD " + url
)
453 # Platform support for Unix
456 # These are the right tests because all these Unix browsers require either
457 # a console terminal or an X display to run.
459 def register_X_browsers():
461 # use xdg-open if around
462 if shutil
.which("xdg-open"):
463 register("xdg-open", None, BackgroundBrowser("xdg-open"))
465 # Opens an appropriate browser for the URL scheme according to
466 # freedesktop.org settings (GNOME, KDE, XFCE, etc.)
467 if shutil
.which("gio"):
468 register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"]))
470 # The default KDE browser
471 if "KDE_FULL_SESSION" in os
.environ
and shutil
.which("kfmclient"):
472 register("kfmclient", Konqueror
, Konqueror("kfmclient"))
474 if shutil
.which("x-www-browser"):
475 register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
477 # The Mozilla browsers
478 for browser
in ("firefox", "iceweasel", "iceape", "seamonkey"):
479 if shutil
.which(browser
):
480 register(browser
, None, Mozilla(browser
))
482 # The Netscape and old Mozilla browsers
483 for browser
in ("mozilla-firefox",
484 "mozilla-firebird", "firebird",
485 "mozilla", "netscape"):
486 if shutil
.which(browser
):
487 register(browser
, None, Netscape(browser
))
489 # Konqueror/kfm, the KDE browser.
490 if shutil
.which("kfm"):
491 register("kfm", Konqueror
, Konqueror("kfm"))
492 elif shutil
.which("konqueror"):
493 register("konqueror", Konqueror
, Konqueror("konqueror"))
495 # Gnome's Galeon and Epiphany
496 for browser
in ("galeon", "epiphany"):
497 if shutil
.which(browser
):
498 register(browser
, None, Galeon(browser
))
500 # Skipstone, another Gtk/Mozilla based browser
501 if shutil
.which("skipstone"):
502 register("skipstone", None, BackgroundBrowser("skipstone"))
504 # Google Chrome/Chromium browsers
505 for browser
in ("google-chrome", "chrome", "chromium", "chromium-browser"):
506 if shutil
.which(browser
):
507 register(browser
, None, Chrome(browser
))
509 # Opera, quite popular
510 if shutil
.which("opera"):
511 register("opera", None, Opera("opera"))
513 # Next, Mosaic -- old but still in use.
514 if shutil
.which("mosaic"):
515 register("mosaic", None, BackgroundBrowser("mosaic"))
517 # Grail, the Python browser. Does anybody still use it?
518 if shutil
.which("grail"):
519 register("grail", Grail
, None)
521 def register_standard_browsers():
525 if sys
.platform
== 'darwin':
526 register("MacOSX", None, MacOSXOSAScript('default'))
527 register("chrome", None, MacOSXOSAScript('chrome'))
528 register("firefox", None, MacOSXOSAScript('firefox'))
529 register("safari", None, MacOSXOSAScript('safari'))
530 # OS X can use below Unix support (but we prefer using the OS X
533 if sys
.platform
== "serenityos":
534 # SerenityOS webbrowser, simply called "Browser".
535 register("Browser", None, BackgroundBrowser("Browser"))
537 if sys
.platform
[:3] == "win":
538 # First try to use the default Windows browser
539 register("windows-default", WindowsDefault
)
541 # Detect some common Windows browsers, fallback to IE
542 iexplore
= os
.path
.join(os
.environ
.get("PROGRAMFILES", "C:\\Program Files"),
543 "Internet Explorer\\IEXPLORE.EXE")
544 for browser
in ("firefox", "firebird", "seamonkey", "mozilla",
545 "netscape", "opera", iexplore
):
546 if shutil
.which(browser
):
547 register(browser
, None, BackgroundBrowser(browser
))
549 # Prefer X browsers if present
550 if os
.environ
.get("DISPLAY") or os
.environ
.get("WAYLAND_DISPLAY"):
552 cmd
= "xdg-settings get default-web-browser".split()
553 raw_result
= subprocess
.check_output(cmd
, stderr
=subprocess
.DEVNULL
)
554 result
= raw_result
.decode().strip()
555 except (FileNotFoundError
, subprocess
.CalledProcessError
, PermissionError
, NotADirectoryError
) :
558 global _os_preferred_browser
559 _os_preferred_browser
= result
561 register_X_browsers()
563 # Also try console browsers
564 if os
.environ
.get("TERM"):
565 if shutil
.which("www-browser"):
566 register("www-browser", None, GenericBrowser("www-browser"))
567 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
568 if shutil
.which("links"):
569 register("links", None, GenericBrowser("links"))
570 if shutil
.which("elinks"):
571 register("elinks", None, Elinks("elinks"))
572 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
573 if shutil
.which("lynx"):
574 register("lynx", None, GenericBrowser("lynx"))
575 # The w3m browser <http://w3m.sourceforge.net/>
576 if shutil
.which("w3m"):
577 register("w3m", None, GenericBrowser("w3m"))
579 # OK, now that we know what the default preference orders for each
580 # platform are, allow user to override them with the BROWSER variable.
581 if "BROWSER" in os
.environ
:
582 userchoices
= os
.environ
["BROWSER"].split(os
.pathsep
)
583 userchoices
.reverse()
585 # Treat choices in same way as if passed into get() but do register
586 # and prepend to _tryorder
587 for cmdline
in userchoices
:
589 cmd
= _synthesize(cmdline
, preferred
=True)
591 register(cmdline
, None, GenericBrowser(cmdline
), preferred
=True)
593 # what to do if _tryorder is now empty?
597 # Platform support for Windows
600 if sys
.platform
[:3] == "win":
601 class WindowsDefault(BaseBrowser
):
602 def open(self
, url
, new
=0, autoraise
=True):
603 sys
.audit("webbrowser.open", url
)
607 # [Error 22] No application is associated with the specified
608 # file for this operation: '<URL>'
614 # Platform support for MacOS
617 if sys
.platform
== 'darwin':
618 # Adapted from patch submitted to SourceForge by Steven J. Burr
619 class MacOSX(BaseBrowser
):
620 """Launcher class for Aqua browsers on Mac OS X
622 Optionally specify a browser name on instantiation. Note that this
623 will not work for Aqua browsers if the user has moved the application
624 package after installation.
626 If no browser is specified, the default browser, as specified in the
627 Internet System Preferences panel, will be used.
629 def __init__(self
, name
):
630 warnings
.warn(f
'{self.__class__.__name__} is deprecated in 3.11'
631 ' use MacOSXOSAScript instead.', DeprecationWarning, stacklevel
=2)
634 def open(self
, url
, new
=0, autoraise
=True):
635 sys
.audit("webbrowser.open", url
)
636 assert "'" not in url
637 # hack for local urls
643 if self
.name
== "default":
644 # User called open, open_new or get without a browser parameter
645 script
= 'open location "%s"' % url
.replace('"', '%22') # opens in default browser
647 # User called get and chose a browser
648 if self
.name
== "OmniWeb":
651 # Include toWindow parameter of OpenURL command for browsers
652 # that support it. 0 == new window; -1 == existing
653 toWindow
= "toWindow %d" % (new
- 1)
654 cmd
= 'OpenURL "%s"' % url
.replace('"', '%22')
655 script
= '''tell application "%s"
658 end tell''' % (self
.name
, cmd
, toWindow
)
659 # Open pipe to AppleScript through osascript command
660 osapipe
= os
.popen("osascript", "w")
663 # Write script to osascript's stdin
664 osapipe
.write(script
)
668 class MacOSXOSAScript(BaseBrowser
):
669 def __init__(self
, name
):
672 def open(self
, url
, new
=0, autoraise
=True):
673 if self
._name
== 'default':
674 script
= 'open location "%s"' % url
.replace('"', '%22') # opens in default browser
677 tell application "%s"
681 '''%(self
._name
, url
.replace('"', '%22'))
683 osapipe
= os
.popen("osascript", "w")
687 osapipe
.write(script
)
694 usage
= """Usage: %s [-n | -t] url
696 -t: open new tab""" % sys
.argv
[0]
698 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'ntd')
699 except getopt
.error
as msg
:
700 print(msg
, file=sys
.stderr
)
701 print(usage
, file=sys
.stderr
)
705 if o
== '-n': new_win
= 1
706 elif o
== '-t': new_win
= 2
708 print(usage
, file=sys
.stderr
)
716 if __name__
== "__main__":