Back to index

python3.2  3.2.2
webbrowser.py
Go to the documentation of this file.
00001 #! /usr/bin/env python3
00002 """Interfaces for launching and remotely controlling Web browsers."""
00003 # Maintained by Georg Brandl.
00004 
00005 import io
00006 import os
00007 import shlex
00008 import sys
00009 import stat
00010 import subprocess
00011 import time
00012 
00013 __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
00014 
00015 class Error(Exception):
00016     pass
00017 
00018 _browsers = {}          # Dictionary of available browser controllers
00019 _tryorder = []          # Preference order of available browsers
00020 
00021 def register(name, klass, instance=None, update_tryorder=1):
00022     """Register a browser connector and, optionally, connection."""
00023     _browsers[name.lower()] = [klass, instance]
00024     if update_tryorder > 0:
00025         _tryorder.append(name)
00026     elif update_tryorder < 0:
00027         _tryorder.insert(0, name)
00028 
00029 def get(using=None):
00030     """Return a browser launcher instance appropriate for the environment."""
00031     if using is not None:
00032         alternatives = [using]
00033     else:
00034         alternatives = _tryorder
00035     for browser in alternatives:
00036         if '%s' in browser:
00037             # User gave us a command line, split it into name and args
00038             browser = shlex.split(browser)
00039             if browser[-1] == '&':
00040                 return BackgroundBrowser(browser[:-1])
00041             else:
00042                 return GenericBrowser(browser)
00043         else:
00044             # User gave us a browser name or path.
00045             try:
00046                 command = _browsers[browser.lower()]
00047             except KeyError:
00048                 command = _synthesize(browser)
00049             if command[1] is not None:
00050                 return command[1]
00051             elif command[0] is not None:
00052                 return command[0]()
00053     raise Error("could not locate runnable browser")
00054 
00055 # Please note: the following definition hides a builtin function.
00056 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
00057 # instead of "from webbrowser import *".
00058 
00059 def open(url, new=0, autoraise=True):
00060     for name in _tryorder:
00061         browser = get(name)
00062         if browser.open(url, new, autoraise):
00063             return True
00064     return False
00065 
00066 def open_new(url):
00067     return open(url, 1)
00068 
00069 def open_new_tab(url):
00070     return open(url, 2)
00071 
00072 
00073 def _synthesize(browser, update_tryorder=1):
00074     """Attempt to synthesize a controller base on existing controllers.
00075 
00076     This is useful to create a controller when a user specifies a path to
00077     an entry in the BROWSER environment variable -- we can copy a general
00078     controller to operate using a specific installation of the desired
00079     browser in this way.
00080 
00081     If we can't create a controller in this way, or if there is no
00082     executable for the requested browser, return [None, None].
00083 
00084     """
00085     cmd = browser.split()[0]
00086     if not _iscommand(cmd):
00087         return [None, None]
00088     name = os.path.basename(cmd)
00089     try:
00090         command = _browsers[name.lower()]
00091     except KeyError:
00092         return [None, None]
00093     # now attempt to clone to fit the new name:
00094     controller = command[1]
00095     if controller and name.lower() == controller.basename:
00096         import copy
00097         controller = copy.copy(controller)
00098         controller.name = browser
00099         controller.basename = os.path.basename(browser)
00100         register(browser, None, controller, update_tryorder)
00101         return [None, controller]
00102     return [None, None]
00103 
00104 
00105 if sys.platform[:3] == "win":
00106     def _isexecutable(cmd):
00107         cmd = cmd.lower()
00108         if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
00109             return True
00110         for ext in ".exe", ".bat":
00111             if os.path.isfile(cmd + ext):
00112                 return True
00113         return False
00114 else:
00115     def _isexecutable(cmd):
00116         if os.path.isfile(cmd):
00117             mode = os.stat(cmd)[stat.ST_MODE]
00118             if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
00119                 return True
00120         return False
00121 
00122 def _iscommand(cmd):
00123     """Return True if cmd is executable or can be found on the executable
00124     search path."""
00125     if _isexecutable(cmd):
00126         return True
00127     path = os.environ.get("PATH")
00128     if not path:
00129         return False
00130     for d in path.split(os.pathsep):
00131         exe = os.path.join(d, cmd)
00132         if _isexecutable(exe):
00133             return True
00134     return False
00135 
00136 
00137 # General parent classes
00138 
00139 class BaseBrowser(object):
00140     """Parent class for all browsers. Do not use directly."""
00141 
00142     args = ['%s']
00143 
00144     def __init__(self, name=""):
00145         self.name = name
00146         self.basename = name
00147 
00148     def open(self, url, new=0, autoraise=True):
00149         raise NotImplementedError
00150 
00151     def open_new(self, url):
00152         return self.open(url, 1)
00153 
00154     def open_new_tab(self, url):
00155         return self.open(url, 2)
00156 
00157 
00158 class GenericBrowser(BaseBrowser):
00159     """Class for all browsers started with a command
00160        and without remote functionality."""
00161 
00162     def __init__(self, name):
00163         if isinstance(name, str):
00164             self.name = name
00165             self.args = ["%s"]
00166         else:
00167             # name should be a list with arguments
00168             self.name = name[0]
00169             self.args = name[1:]
00170         self.basename = os.path.basename(self.name)
00171 
00172     def open(self, url, new=0, autoraise=True):
00173         cmdline = [self.name] + [arg.replace("%s", url)
00174                                  for arg in self.args]
00175         try:
00176             if sys.platform[:3] == 'win':
00177                 p = subprocess.Popen(cmdline)
00178             else:
00179                 p = subprocess.Popen(cmdline, close_fds=True)
00180             return not p.wait()
00181         except OSError:
00182             return False
00183 
00184 
00185 class BackgroundBrowser(GenericBrowser):
00186     """Class for all browsers which are to be started in the
00187        background."""
00188 
00189     def open(self, url, new=0, autoraise=True):
00190         cmdline = [self.name] + [arg.replace("%s", url)
00191                                  for arg in self.args]
00192         try:
00193             if sys.platform[:3] == 'win':
00194                 p = subprocess.Popen(cmdline)
00195             else:
00196                 setsid = getattr(os, 'setsid', None)
00197                 if not setsid:
00198                     setsid = getattr(os, 'setpgrp', None)
00199                 p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
00200             return (p.poll() is None)
00201         except OSError:
00202             return False
00203 
00204 
00205 class UnixBrowser(BaseBrowser):
00206     """Parent class for all Unix browsers with remote functionality."""
00207 
00208     raise_opts = None
00209     remote_args = ['%action', '%s']
00210     remote_action = None
00211     remote_action_newwin = None
00212     remote_action_newtab = None
00213     background = False
00214     redirect_stdout = True
00215 
00216     def _invoke(self, args, remote, autoraise):
00217         raise_opt = []
00218         if remote and self.raise_opts:
00219             # use autoraise argument only for remote invocation
00220             autoraise = int(autoraise)
00221             opt = self.raise_opts[autoraise]
00222             if opt: raise_opt = [opt]
00223 
00224         cmdline = [self.name] + raise_opt + args
00225 
00226         if remote or self.background:
00227             inout = io.open(os.devnull, "r+")
00228         else:
00229             # for TTY browsers, we need stdin/out
00230             inout = None
00231         p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
00232                              stdout=(self.redirect_stdout and inout or None),
00233                              stderr=inout, start_new_session=True)
00234         if remote:
00235             # wait five seconds. If the subprocess is not finished, the
00236             # remote invocation has (hopefully) started a new instance.
00237             time.sleep(1)
00238             rc = p.poll()
00239             if rc is None:
00240                 time.sleep(4)
00241                 rc = p.poll()
00242                 if rc is None:
00243                     return True
00244             # if remote call failed, open() will try direct invocation
00245             return not rc
00246         elif self.background:
00247             if p.poll() is None:
00248                 return True
00249             else:
00250                 return False
00251         else:
00252             return not p.wait()
00253 
00254     def open(self, url, new=0, autoraise=True):
00255         if new == 0:
00256             action = self.remote_action
00257         elif new == 1:
00258             action = self.remote_action_newwin
00259         elif new == 2:
00260             if self.remote_action_newtab is None:
00261                 action = self.remote_action_newwin
00262             else:
00263                 action = self.remote_action_newtab
00264         else:
00265             raise Error("Bad 'new' parameter to open(); " +
00266                         "expected 0, 1, or 2, got %s" % new)
00267 
00268         args = [arg.replace("%s", url).replace("%action", action)
00269                 for arg in self.remote_args]
00270         success = self._invoke(args, True, autoraise)
00271         if not success:
00272             # remote invocation failed, try straight way
00273             args = [arg.replace("%s", url) for arg in self.args]
00274             return self._invoke(args, False, False)
00275         else:
00276             return True
00277 
00278 
00279 class Mozilla(UnixBrowser):
00280     """Launcher class for Mozilla/Netscape browsers."""
00281 
00282     raise_opts = ["-noraise", "-raise"]
00283     remote_args = ['-remote', 'openURL(%s%action)']
00284     remote_action = ""
00285     remote_action_newwin = ",new-window"
00286     remote_action_newtab = ",new-tab"
00287     background = True
00288 
00289 Netscape = Mozilla
00290 
00291 
00292 class Galeon(UnixBrowser):
00293     """Launcher class for Galeon/Epiphany browsers."""
00294 
00295     raise_opts = ["-noraise", ""]
00296     remote_args = ['%action', '%s']
00297     remote_action = "-n"
00298     remote_action_newwin = "-w"
00299     background = True
00300 
00301 
00302 class Opera(UnixBrowser):
00303     "Launcher class for Opera browser."
00304 
00305     raise_opts = ["-noraise", ""]
00306     remote_args = ['-remote', 'openURL(%s%action)']
00307     remote_action = ""
00308     remote_action_newwin = ",new-window"
00309     remote_action_newtab = ",new-page"
00310     background = True
00311 
00312 
00313 class Elinks(UnixBrowser):
00314     "Launcher class for Elinks browsers."
00315 
00316     remote_args = ['-remote', 'openURL(%s%action)']
00317     remote_action = ""
00318     remote_action_newwin = ",new-window"
00319     remote_action_newtab = ",new-tab"
00320     background = False
00321 
00322     # elinks doesn't like its stdout to be redirected -
00323     # it uses redirected stdout as a signal to do -dump
00324     redirect_stdout = False
00325 
00326 
00327 class Konqueror(BaseBrowser):
00328     """Controller for the KDE File Manager (kfm, or Konqueror).
00329 
00330     See the output of ``kfmclient --commands``
00331     for more information on the Konqueror remote-control interface.
00332     """
00333 
00334     def open(self, url, new=0, autoraise=True):
00335         # XXX Currently I know no way to prevent KFM from opening a new win.
00336         if new == 2:
00337             action = "newTab"
00338         else:
00339             action = "openURL"
00340 
00341         devnull = io.open(os.devnull, "r+")
00342         # if possible, put browser in separate process group, so
00343         # keyboard interrupts don't affect browser as well as Python
00344         setsid = getattr(os, 'setsid', None)
00345         if not setsid:
00346             setsid = getattr(os, 'setpgrp', None)
00347 
00348         try:
00349             p = subprocess.Popen(["kfmclient", action, url],
00350                                  close_fds=True, stdin=devnull,
00351                                  stdout=devnull, stderr=devnull)
00352         except OSError:
00353             # fall through to next variant
00354             pass
00355         else:
00356             p.wait()
00357             # kfmclient's return code unfortunately has no meaning as it seems
00358             return True
00359 
00360         try:
00361             p = subprocess.Popen(["konqueror", "--silent", url],
00362                                  close_fds=True, stdin=devnull,
00363                                  stdout=devnull, stderr=devnull,
00364                                  preexec_fn=setsid)
00365         except OSError:
00366             # fall through to next variant
00367             pass
00368         else:
00369             if p.poll() is None:
00370                 # Should be running now.
00371                 return True
00372 
00373         try:
00374             p = subprocess.Popen(["kfm", "-d", url],
00375                                  close_fds=True, stdin=devnull,
00376                                  stdout=devnull, stderr=devnull,
00377                                  preexec_fn=setsid)
00378         except OSError:
00379             return False
00380         else:
00381             return (p.poll() is None)
00382 
00383 
00384 class Grail(BaseBrowser):
00385     # There should be a way to maintain a connection to Grail, but the
00386     # Grail remote control protocol doesn't really allow that at this
00387     # point.  It probably never will!
00388     def _find_grail_rc(self):
00389         import glob
00390         import pwd
00391         import socket
00392         import tempfile
00393         tempdir = os.path.join(tempfile.gettempdir(),
00394                                ".grail-unix")
00395         user = pwd.getpwuid(os.getuid())[0]
00396         filename = os.path.join(tempdir, user + "-*")
00397         maybes = glob.glob(filename)
00398         if not maybes:
00399             return None
00400         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
00401         for fn in maybes:
00402             # need to PING each one until we find one that's live
00403             try:
00404                 s.connect(fn)
00405             except socket.error:
00406                 # no good; attempt to clean it out, but don't fail:
00407                 try:
00408                     os.unlink(fn)
00409                 except IOError:
00410                     pass
00411             else:
00412                 return s
00413 
00414     def _remote(self, action):
00415         s = self._find_grail_rc()
00416         if not s:
00417             return 0
00418         s.send(action)
00419         s.close()
00420         return 1
00421 
00422     def open(self, url, new=0, autoraise=True):
00423         if new:
00424             ok = self._remote("LOADNEW " + url)
00425         else:
00426             ok = self._remote("LOAD " + url)
00427         return ok
00428 
00429 
00430 #
00431 # Platform support for Unix
00432 #
00433 
00434 # These are the right tests because all these Unix browsers require either
00435 # a console terminal or an X display to run.
00436 
00437 def register_X_browsers():
00438 
00439     # The default GNOME browser
00440     if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"):
00441         register("gnome-open", None, BackgroundBrowser("gnome-open"))
00442 
00443     # The default KDE browser
00444     if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"):
00445         register("kfmclient", Konqueror, Konqueror("kfmclient"))
00446 
00447     # The Mozilla/Netscape browsers
00448     for browser in ("mozilla-firefox", "firefox",
00449                     "mozilla-firebird", "firebird",
00450                     "seamonkey", "mozilla", "netscape"):
00451         if _iscommand(browser):
00452             register(browser, None, Mozilla(browser))
00453 
00454     # Konqueror/kfm, the KDE browser.
00455     if _iscommand("kfm"):
00456         register("kfm", Konqueror, Konqueror("kfm"))
00457     elif _iscommand("konqueror"):
00458         register("konqueror", Konqueror, Konqueror("konqueror"))
00459 
00460     # Gnome's Galeon and Epiphany
00461     for browser in ("galeon", "epiphany"):
00462         if _iscommand(browser):
00463             register(browser, None, Galeon(browser))
00464 
00465     # Skipstone, another Gtk/Mozilla based browser
00466     if _iscommand("skipstone"):
00467         register("skipstone", None, BackgroundBrowser("skipstone"))
00468 
00469     # Opera, quite popular
00470     if _iscommand("opera"):
00471         register("opera", None, Opera("opera"))
00472 
00473     # Next, Mosaic -- old but still in use.
00474     if _iscommand("mosaic"):
00475         register("mosaic", None, BackgroundBrowser("mosaic"))
00476 
00477     # Grail, the Python browser. Does anybody still use it?
00478     if _iscommand("grail"):
00479         register("grail", Grail, None)
00480 
00481 # Prefer X browsers if present
00482 if os.environ.get("DISPLAY"):
00483     register_X_browsers()
00484 
00485 # Also try console browsers
00486 if os.environ.get("TERM"):
00487     # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
00488     if _iscommand("links"):
00489         register("links", None, GenericBrowser("links"))
00490     if _iscommand("elinks"):
00491         register("elinks", None, Elinks("elinks"))
00492     # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
00493     if _iscommand("lynx"):
00494         register("lynx", None, GenericBrowser("lynx"))
00495     # The w3m browser <http://w3m.sourceforge.net/>
00496     if _iscommand("w3m"):
00497         register("w3m", None, GenericBrowser("w3m"))
00498 
00499 #
00500 # Platform support for Windows
00501 #
00502 
00503 if sys.platform[:3] == "win":
00504     class WindowsDefault(BaseBrowser):
00505         def open(self, url, new=0, autoraise=True):
00506             try:
00507                 os.startfile(url)
00508             except WindowsError:
00509                 # [Error 22] No application is associated with the specified
00510                 # file for this operation: '<URL>'
00511                 return False
00512             else:
00513                 return True
00514 
00515     _tryorder = []
00516     _browsers = {}
00517 
00518     # First try to use the default Windows browser
00519     register("windows-default", WindowsDefault)
00520 
00521     # Detect some common Windows browsers, fallback to IE
00522     iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
00523                             "Internet Explorer\\IEXPLORE.EXE")
00524     for browser in ("firefox", "firebird", "seamonkey", "mozilla",
00525                     "netscape", "opera", iexplore):
00526         if _iscommand(browser):
00527             register(browser, None, BackgroundBrowser(browser))
00528 
00529 #
00530 # Platform support for MacOS
00531 #
00532 
00533 if sys.platform == 'darwin':
00534     # Adapted from patch submitted to SourceForge by Steven J. Burr
00535     class MacOSX(BaseBrowser):
00536         """Launcher class for Aqua browsers on Mac OS X
00537 
00538         Optionally specify a browser name on instantiation.  Note that this
00539         will not work for Aqua browsers if the user has moved the application
00540         package after installation.
00541 
00542         If no browser is specified, the default browser, as specified in the
00543         Internet System Preferences panel, will be used.
00544         """
00545         def __init__(self, name):
00546             self.name = name
00547 
00548         def open(self, url, new=0, autoraise=True):
00549             assert "'" not in url
00550             # hack for local urls
00551             if not ':' in url:
00552                 url = 'file:'+url
00553 
00554             # new must be 0 or 1
00555             new = int(bool(new))
00556             if self.name == "default":
00557                 # User called open, open_new or get without a browser parameter
00558                 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
00559             else:
00560                 # User called get and chose a browser
00561                 if self.name == "OmniWeb":
00562                     toWindow = ""
00563                 else:
00564                     # Include toWindow parameter of OpenURL command for browsers
00565                     # that support it.  0 == new window; -1 == existing
00566                     toWindow = "toWindow %d" % (new - 1)
00567                 cmd = 'OpenURL "%s"' % url.replace('"', '%22')
00568                 script = '''tell application "%s"
00569                                 activate
00570                                 %s %s
00571                             end tell''' % (self.name, cmd, toWindow)
00572             # Open pipe to AppleScript through osascript command
00573             osapipe = os.popen("osascript", "w")
00574             if osapipe is None:
00575                 return False
00576             # Write script to osascript's stdin
00577             osapipe.write(script)
00578             rc = osapipe.close()
00579             return not rc
00580 
00581     class MacOSXOSAScript(BaseBrowser):
00582         def __init__(self, name):
00583             self._name = name
00584 
00585         def open(self, url, new=0, autoraise=True):
00586             if self._name == 'default':
00587                 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
00588             else:
00589                 script = '''
00590                    tell application "%s"
00591                        activate
00592                        open location "%s"
00593                    end
00594                    '''%(self._name, url.replace('"', '%22'))
00595 
00596             osapipe = os.popen("osascript", "w")
00597             if osapipe is None:
00598                 return False
00599 
00600             osapipe.write(script)
00601             rc = osapipe.close()
00602             return not rc
00603 
00604 
00605     # Don't clear _tryorder or _browsers since OS X can use above Unix support
00606     # (but we prefer using the OS X specific stuff)
00607     register("safari", None, MacOSXOSAScript('safari'), -1)
00608     register("firefox", None, MacOSXOSAScript('firefox'), -1)
00609     register("MacOSX", None, MacOSXOSAScript('default'), -1)
00610 
00611 
00612 #
00613 # Platform support for OS/2
00614 #
00615 
00616 if sys.platform[:3] == "os2" and _iscommand("netscape"):
00617     _tryorder = []
00618     _browsers = {}
00619     register("os2netscape", None,
00620              GenericBrowser(["start", "netscape", "%s"]), -1)
00621 
00622 
00623 # OK, now that we know what the default preference orders for each
00624 # platform are, allow user to override them with the BROWSER variable.
00625 if "BROWSER" in os.environ:
00626     _userchoices = os.environ["BROWSER"].split(os.pathsep)
00627     _userchoices.reverse()
00628 
00629     # Treat choices in same way as if passed into get() but do register
00630     # and prepend to _tryorder
00631     for cmdline in _userchoices:
00632         if cmdline != '':
00633             cmd = _synthesize(cmdline, -1)
00634             if cmd[1] is None:
00635                 register(cmdline, None, GenericBrowser(cmdline), -1)
00636     cmdline = None # to make del work if _userchoices was empty
00637     del cmdline
00638     del _userchoices
00639 
00640 # what to do if _tryorder is now empty?
00641 
00642 
00643 def main():
00644     import getopt
00645     usage = """Usage: %s [-n | -t] url
00646     -n: open new window
00647     -t: open new tab""" % sys.argv[0]
00648     try:
00649         opts, args = getopt.getopt(sys.argv[1:], 'ntd')
00650     except getopt.error as msg:
00651         print(msg, file=sys.stderr)
00652         print(usage, file=sys.stderr)
00653         sys.exit(1)
00654     new_win = 0
00655     for o, a in opts:
00656         if o == '-n': new_win = 1
00657         elif o == '-t': new_win = 2
00658     if len(args) != 1:
00659         print(usage, file=sys.stderr)
00660         sys.exit(1)
00661 
00662     url = args[0]
00663     open(url, new_win)
00664 
00665     print("\a")
00666 
00667 if __name__ == "__main__":
00668     main()