"""Interfaces for launching and remotely controlling Web browsers."""
# Maintained by Georg Brandl.
__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
_lock = threading.RLock()
_browsers = {} # Dictionary of available browser controllers
_tryorder = None # Preference order of available browsers
_os_preferred_browser = None # The preferred browser
def register(name, klass, instance=None, *, preferred=False):
"""Register a browser connector."""
register_standard_browsers()
_browsers[name.lower()] = [klass, instance]
# Preferred browsers go to the front of the list.
# Need to match to the default browser returned by xdg-settings, which
# may be of the form e.g. "firefox.desktop".
if preferred or (_os_preferred_browser and name in _os_preferred_browser):
_tryorder.insert(0, name)
"""Return a browser launcher instance appropriate for the environment."""
register_standard_browsers()
for browser in alternatives:
# User gave us a command line, split it into name and args
browser = shlex.split(browser)
return BackgroundBrowser(browser[:-1])
return GenericBrowser(browser)
# User gave us a browser name or path.
command = _browsers[browser.lower()]
command = _synthesize(browser)
if command[1] is not None:
elif command[0] is not None:
raise Error("could not locate runnable browser")
# Please note: the following definition hides a builtin function.
# It is recommended one does "import webbrowser" and uses webbrowser.open(url)
# instead of "from webbrowser import *".
def open(url, new=0, autoraise=True):
"""Display url using the default browser.
If possible, open url in a location determined by new.
- 0: the same browser window (the default).
- 1: a new browser window.
- 2: a new browser page ("tab").
If possible, autoraise raises the window (the default) or not.
register_standard_browsers()
if browser.open(url, new, autoraise):
"""Open url in a new window of the default browser.
If not possible, then open url in the only browser window.
"""Open url in a new page ("tab") of the default browser.
If not possible, then the behavior becomes equivalent to open_new().
def _synthesize(browser, *, preferred=False):
"""Attempt to synthesize a controller based on existing controllers.
This is useful to create a controller when a user specifies a path to
an entry in the BROWSER environment variable -- we can copy a general
controller to operate using a specific installation of the desired
If we can't create a controller in this way, or if there is no
executable for the requested browser, return [None, None].
if not shutil.which(cmd):
name = os.path.basename(cmd)
command = _browsers[name.lower()]
# now attempt to clone to fit the new name:
if controller and name.lower() == controller.basename:
controller = copy.copy(controller)
controller.name = browser
controller.basename = os.path.basename(browser)
register(browser, None, instance=controller, preferred=preferred)
return [None, controller]
class BaseBrowser(object):
"""Parent class for all browsers. Do not use directly."""
def __init__(self, name=""):
def open(self, url, new=0, autoraise=True):
raise NotImplementedError
def open_new_tab(self, url):
class GenericBrowser(BaseBrowser):
"""Class for all browsers started with a command
and without remote functionality."""
def __init__(self, name):
if isinstance(name, str):
# name should be a list with arguments
self.basename = os.path.basename(self.name)
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
cmdline = [self.name] + [arg.replace("%s", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
p = subprocess.Popen(cmdline, close_fds=True)
class BackgroundBrowser(GenericBrowser):
"""Class for all browsers which are to be started in the
def open(self, url, new=0, autoraise=True):
cmdline = [self.name] + [arg.replace("%s", url)
sys.audit("webbrowser.open", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
p = subprocess.Popen(cmdline, close_fds=True,
return (p.poll() is None)
class UnixBrowser(BaseBrowser):
"""Parent class for all Unix browsers with remote functionality."""
# In remote_args, %s will be replaced with the requested URL. %action will
# be replaced depending on the value of 'new' passed to open.
# remote_action is used for new=0 (open). If newwin is not None, it is
# used for new=1 (open_new). If newtab is not None, it is used for
# new=3 (open_new_tab). After both substitutions are made, any empty
# strings in the transformed remote_args list will be removed.
remote_args = ['%action', '%s']
remote_action_newwin = None
remote_action_newtab = None
def _invoke(self, args, remote, autoraise, url=None):
if remote and self.raise_opts:
# use autoraise argument only for remote invocation
autoraise = int(autoraise)
opt = self.raise_opts[autoraise]
if opt: raise_opt = [opt]
cmdline = [self.name] + raise_opt + args
if remote or self.background:
inout = subprocess.DEVNULL
# for TTY browsers, we need stdin/out
p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
stdout=(self.redirect_stdout and inout or None),
stderr=inout, start_new_session=True)
# wait at most five seconds. If the subprocess is not finished, the
# remote invocation has (hopefully) started a new instance.
# if remote call failed, open() will try direct invocation
except subprocess.TimeoutExpired:
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
action = self.remote_action
action = self.remote_action_newwin
if self.remote_action_newtab is None:
action = self.remote_action_newwin
action = self.remote_action_newtab
raise Error("Bad 'new' parameter to open(); " +
"expected 0, 1, or 2, got %s" % new)
args = [arg.replace("%s", url).replace("%action", action)
for arg in self.remote_args]
args = [arg for arg in args if arg]
success = self._invoke(args, True, autoraise, url)
# remote invocation failed, try straight way
args = [arg.replace("%s", url) for arg in self.args]
return self._invoke(args, False, False)
class Mozilla(UnixBrowser):
"""Launcher class for Mozilla browsers."""
remote_args = ['%action', '%s']
remote_action_newwin = "-new-window"
remote_action_newtab = "-new-tab"
class Netscape(UnixBrowser):
"""Launcher class for Netscape browser."""
raise_opts = ["-noraise", "-raise"]
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
class Galeon(UnixBrowser):
"""Launcher class for Galeon/Epiphany browsers."""
raise_opts = ["-noraise", ""]
remote_args = ['%action', '%s']
remote_action_newwin = "-w"
class Chrome(UnixBrowser):
"Launcher class for Google Chrome browser."
remote_args = ['%action', '%s']
remote_action_newwin = "--new-window"
remote_action_newtab = ""
class Opera(UnixBrowser):
"Launcher class for Opera browser."
remote_args = ['%action', '%s']
remote_action_newwin = "--new-window"
remote_action_newtab = ""
class Elinks(UnixBrowser):
"Launcher class for Elinks browsers."
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
# elinks doesn't like its stdout to be redirected -
# it uses redirected stdout as a signal to do -dump
class Konqueror(BaseBrowser):
"""Controller for the KDE File Manager (kfm, or Konqueror).
See the output of ``kfmclient --commands``
for more information on the Konqueror remote-control interface.
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
# XXX Currently I know no way to prevent KFM from opening a new win.
devnull = subprocess.DEVNULL
p = subprocess.Popen(["kfmclient", action, url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull)
# fall through to next variant
# kfmclient's return code unfortunately has no meaning as it seems
p = subprocess.Popen(["konqueror", "--silent", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
# fall through to next variant
p = subprocess.Popen(["kfm", "-d", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
return (p.poll() is None)
class Grail(BaseBrowser):
# There should be a way to maintain a connection to Grail, but the
# Grail remote control protocol doesn't really allow that at this
# point. It probably never will!
def _find_grail_rc(self):
tempdir = os.path.join(tempfile.gettempdir(),
user = pwd.getpwuid(os.getuid())[0]
filename = os.path.join(glob.escape(tempdir), glob.escape(user) + "-*")
maybes = glob.glob(filename)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# need to PING each one until we find one that's live
# no good; attempt to clean it out, but don't fail:
def _remote(self, action):
s = self._find_grail_rc()
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
ok = self._remote("LOADNEW " + url)
ok = self._remote("LOAD " + url)
# Platform support for Unix
# These are the right tests because all these Unix browsers require either
# a console terminal or an X display to run.
def register_X_browsers():
if shutil.which("xdg-open"):
register("xdg-open", None, BackgroundBrowser("xdg-open"))
# The default GNOME3 browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gvfs-open"):
register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
# The default GNOME browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gnome-open"):
register("gnome-open", None, BackgroundBrowser("gnome-open"))
# The default KDE browser
if "KDE_FULL_SESSION" in os.environ and shutil.which("kfmclient"):
register("kfmclient", Konqueror, Konqueror("kfmclient"))
if shutil.which("x-www-browser"):
register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
if shutil.which(browser):
register(browser, None, Mozilla(browser))
# The Netscape and old Mozilla browsers
for browser in ("mozilla-firefox",
"mozilla-firebird", "firebird",
if shutil.which(browser):
register(browser, None, Netscape(browser))
# Konqueror/kfm, the KDE browser.
register("kfm", Konqueror, Konqueror("kfm"))
elif shutil.which("konqueror"):
register("konqueror", Konqueror, Konqueror("konqueror"))
# Gnome's Galeon and Epiphany
for browser in ("galeon", "epiphany"):
if shutil.which(browser):
register(browser, None, Galeon(browser))