Home | History | Annotate | Download | only in python2.7
      1 #! /usr/bin/env python
      2 """Interfaces for launching and remotely controlling Web browsers."""
      3 # Maintained by Georg Brandl.
      4 
      5 import os
      6 import shlex
      7 import sys
      8 import stat
      9 import subprocess
     10 import time
     11 
     12 __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
     13 
     14 class Error(Exception):
     15     pass
     16 
     17 _browsers = {}          # Dictionary of available browser controllers
     18 _tryorder = []          # Preference order of available browsers
     19 
     20 def register(name, klass, instance=None, update_tryorder=1):
     21     """Register a browser connector and, optionally, connection."""
     22     _browsers[name.lower()] = [klass, instance]
     23     if update_tryorder > 0:
     24         _tryorder.append(name)
     25     elif update_tryorder < 0:
     26         _tryorder.insert(0, name)
     27 
     28 def get(using=None):
     29     """Return a browser launcher instance appropriate for the environment."""
     30     if using is not None:
     31         alternatives = [using]
     32     else:
     33         alternatives = _tryorder
     34     for browser in alternatives:
     35         if '%s' in browser:
     36             # User gave us a command line, split it into name and args
     37             browser = shlex.split(browser)
     38             if browser[-1] == '&':
     39                 return BackgroundBrowser(browser[:-1])
     40             else:
     41                 return GenericBrowser(browser)
     42         else:
     43             # User gave us a browser name or path.
     44             try:
     45                 command = _browsers[browser.lower()]
     46             except KeyError:
     47                 command = _synthesize(browser)
     48             if command[1] is not None:
     49                 return command[1]
     50             elif command[0] is not None:
     51                 return command[0]()
     52     raise Error("could not locate runnable browser")
     53 
     54 # Please note: the following definition hides a builtin function.
     55 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
     56 # instead of "from webbrowser import *".
     57 
     58 def open(url, new=0, autoraise=True):
     59     for name in _tryorder:
     60         browser = get(name)
     61         if browser.open(url, new, autoraise):
     62             return True
     63     return False
     64 
     65 def open_new(url):
     66     return open(url, 1)
     67 
     68 def open_new_tab(url):
     69     return open(url, 2)
     70 
     71 
     72 def _synthesize(browser, update_tryorder=1):
     73     """Attempt to synthesize a controller base on existing controllers.
     74 
     75     This is useful to create a controller when a user specifies a path to
     76     an entry in the BROWSER environment variable -- we can copy a general
     77     controller to operate using a specific installation of the desired
     78     browser in this way.
     79 
     80     If we can't create a controller in this way, or if there is no
     81     executable for the requested browser, return [None, None].
     82 
     83     """
     84     cmd = browser.split()[0]
     85     if not _iscommand(cmd):
     86         return [None, None]
     87     name = os.path.basename(cmd)
     88     try:
     89         command = _browsers[name.lower()]
     90     except KeyError:
     91         return [None, None]
     92     # now attempt to clone to fit the new name:
     93     controller = command[1]
     94     if controller and name.lower() == controller.basename:
     95         import copy
     96         controller = copy.copy(controller)
     97         controller.name = browser
     98         controller.basename = os.path.basename(browser)
     99         register(browser, None, controller, update_tryorder)
    100         return [None, controller]
    101     return [None, None]
    102 
    103 
    104 if sys.platform[:3] == "win":
    105     def _isexecutable(cmd):
    106         cmd = cmd.lower()
    107         if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
    108             return True
    109         for ext in ".exe", ".bat":
    110             if os.path.isfile(cmd + ext):
    111                 return True
    112         return False
    113 else:
    114     def _isexecutable(cmd):
    115         if os.path.isfile(cmd):
    116             mode = os.stat(cmd)[stat.ST_MODE]
    117             if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
    118                 return True
    119         return False
    120 
    121 def _iscommand(cmd):
    122     """Return True if cmd is executable or can be found on the executable
    123     search path."""
    124     if _isexecutable(cmd):
    125         return True
    126     path = os.environ.get("PATH")
    127     if not path:
    128         return False
    129     for d in path.split(os.pathsep):
    130         exe = os.path.join(d, cmd)
    131         if _isexecutable(exe):
    132             return True
    133     return False
    134 
    135 
    136 # General parent classes
    137 
    138 class BaseBrowser(object):
    139     """Parent class for all browsers. Do not use directly."""
    140 
    141     args = ['%s']
    142 
    143     def __init__(self, name=""):
    144         self.name = name
    145         self.basename = name
    146 
    147     def open(self, url, new=0, autoraise=True):
    148         raise NotImplementedError
    149 
    150     def open_new(self, url):
    151         return self.open(url, 1)
    152 
    153     def open_new_tab(self, url):
    154         return self.open(url, 2)
    155 
    156 
    157 class GenericBrowser(BaseBrowser):
    158     """Class for all browsers started with a command
    159        and without remote functionality."""
    160 
    161     def __init__(self, name):
    162         if isinstance(name, basestring):
    163             self.name = name
    164             self.args = ["%s"]
    165         else:
    166             # name should be a list with arguments
    167             self.name = name[0]
    168             self.args = name[1:]
    169         self.basename = os.path.basename(self.name)
    170 
    171     def open(self, url, new=0, autoraise=True):
    172         cmdline = [self.name] + [arg.replace("%s", url)
    173                                  for arg in self.args]
    174         try:
    175             if sys.platform[:3] == 'win':
    176                 p = subprocess.Popen(cmdline)
    177             else:
    178                 p = subprocess.Popen(cmdline, close_fds=True)
    179             return not p.wait()
    180         except OSError:
    181             return False
    182 
    183 
    184 class BackgroundBrowser(GenericBrowser):
    185     """Class for all browsers which are to be started in the
    186        background."""
    187 
    188     def open(self, url, new=0, autoraise=True):
    189         cmdline = [self.name] + [arg.replace("%s", url)
    190                                  for arg in self.args]
    191         try:
    192             if sys.platform[:3] == 'win':
    193                 p = subprocess.Popen(cmdline)
    194             else:
    195                 setsid = getattr(os, 'setsid', None)
    196                 if not setsid:
    197                     setsid = getattr(os, 'setpgrp', None)
    198                 p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
    199             return (p.poll() is None)
    200         except OSError:
    201             return False
    202 
    203 
    204 class UnixBrowser(BaseBrowser):
    205     """Parent class for all Unix browsers with remote functionality."""
    206 
    207     raise_opts = None
    208     remote_args = ['%action', '%s']
    209     remote_action = None
    210     remote_action_newwin = None
    211     remote_action_newtab = None
    212     background = False
    213     redirect_stdout = True
    214 
    215     def _invoke(self, args, remote, autoraise):
    216         raise_opt = []
    217         if remote and self.raise_opts:
    218             # use autoraise argument only for remote invocation
    219             autoraise = int(autoraise)
    220             opt = self.raise_opts[autoraise]
    221             if opt: raise_opt = [opt]
    222 
    223         cmdline = [self.name] + raise_opt + args
    224 
    225         if remote or self.background:
    226             inout = file(os.devnull, "r+")
    227         else:
    228             # for TTY browsers, we need stdin/out
    229             inout = None
    230         # if possible, put browser in separate process group, so
    231         # keyboard interrupts don't affect browser as well as Python
    232         setsid = getattr(os, 'setsid', None)
    233         if not setsid:
    234             setsid = getattr(os, 'setpgrp', None)
    235 
    236         p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
    237                              stdout=(self.redirect_stdout and inout or None),
    238                              stderr=inout, preexec_fn=setsid)
    239         if remote:
    240             # wait five seconds. If the subprocess is not finished, the
    241             # remote invocation has (hopefully) started a new instance.
    242             time.sleep(1)
    243             rc = p.poll()
    244             if rc is None:
    245                 time.sleep(4)
    246                 rc = p.poll()
    247                 if rc is None:
    248                     return True
    249             # if remote call failed, open() will try direct invocation
    250             return not rc
    251         elif self.background:
    252             if p.poll() is None:
    253                 return True
    254             else:
    255                 return False
    256         else:
    257             return not p.wait()
    258 
    259     def open(self, url, new=0, autoraise=True):
    260         if new == 0:
    261             action = self.remote_action
    262         elif new == 1:
    263             action = self.remote_action_newwin
    264         elif new == 2:
    265             if self.remote_action_newtab is None:
    266                 action = self.remote_action_newwin
    267             else:
    268                 action = self.remote_action_newtab
    269         else:
    270             raise Error("Bad 'new' parameter to open(); " +
    271                         "expected 0, 1, or 2, got %s" % new)
    272 
    273         args = [arg.replace("%s", url).replace("%action", action)
    274                 for arg in self.remote_args]
    275         success = self._invoke(args, True, autoraise)
    276         if not success:
    277             # remote invocation failed, try straight way
    278             args = [arg.replace("%s", url) for arg in self.args]
    279             return self._invoke(args, False, False)
    280         else:
    281             return True
    282 
    283 
    284 class Mozilla(UnixBrowser):
    285     """Launcher class for Mozilla/Netscape browsers."""
    286 
    287     raise_opts = ["-noraise", "-raise"]
    288     remote_args = ['-remote', 'openURL(%s%action)']
    289     remote_action = ""
    290     remote_action_newwin = ",new-window"
    291     remote_action_newtab = ",new-tab"
    292     background = True
    293 
    294 Netscape = Mozilla
    295 
    296 
    297 class Galeon(UnixBrowser):
    298     """Launcher class for Galeon/Epiphany browsers."""
    299 
    300     raise_opts = ["-noraise", ""]
    301     remote_args = ['%action', '%s']
    302     remote_action = "-n"
    303     remote_action_newwin = "-w"
    304     background = True
    305 
    306 
    307 class Chrome(UnixBrowser):
    308     "Launcher class for Google Chrome browser."
    309 
    310     remote_args = ['%action', '%s']
    311     remote_action = ""
    312     remote_action_newwin = "--new-window"
    313     remote_action_newtab = ""
    314     background = True
    315 
    316 Chromium = Chrome
    317 
    318 
    319 class Opera(UnixBrowser):
    320     "Launcher class for Opera browser."
    321 
    322     raise_opts = ["-noraise", ""]
    323     remote_args = ['-remote', 'openURL(%s%action)']
    324     remote_action = ""
    325     remote_action_newwin = ",new-window"
    326     remote_action_newtab = ",new-page"
    327     background = True
    328 
    329 
    330 class Elinks(UnixBrowser):
    331     "Launcher class for Elinks browsers."
    332 
    333     remote_args = ['-remote', 'openURL(%s%action)']
    334     remote_action = ""
    335     remote_action_newwin = ",new-window"
    336     remote_action_newtab = ",new-tab"
    337     background = False
    338 
    339     # elinks doesn't like its stdout to be redirected -
    340     # it uses redirected stdout as a signal to do -dump
    341     redirect_stdout = False
    342 
    343 
    344 class Konqueror(BaseBrowser):
    345     """Controller for the KDE File Manager (kfm, or Konqueror).
    346 
    347     See the output of ``kfmclient --commands``
    348     for more information on the Konqueror remote-control interface.
    349     """
    350 
    351     def open(self, url, new=0, autoraise=True):
    352         # XXX Currently I know no way to prevent KFM from opening a new win.
    353         if new == 2:
    354             action = "newTab"
    355         else:
    356             action = "openURL"
    357 
    358         devnull = file(os.devnull, "r+")
    359         # if possible, put browser in separate process group, so
    360         # keyboard interrupts don't affect browser as well as Python
    361         setsid = getattr(os, 'setsid', None)
    362         if not setsid:
    363             setsid = getattr(os, 'setpgrp', None)
    364 
    365         try:
    366             p = subprocess.Popen(["kfmclient", action, url],
    367                                  close_fds=True, stdin=devnull,
    368                                  stdout=devnull, stderr=devnull)
    369         except OSError:
    370             # fall through to next variant
    371             pass
    372         else:
    373             p.wait()
    374             # kfmclient's return code unfortunately has no meaning as it seems
    375             return True
    376 
    377         try:
    378             p = subprocess.Popen(["konqueror", "--silent", url],
    379                                  close_fds=True, stdin=devnull,
    380                                  stdout=devnull, stderr=devnull,
    381                                  preexec_fn=setsid)
    382         except OSError:
    383             # fall through to next variant
    384             pass
    385         else:
    386             if p.poll() is None:
    387                 # Should be running now.
    388                 return True
    389 
    390         try:
    391             p = subprocess.Popen(["kfm", "-d", url],
    392                                  close_fds=True, stdin=devnull,
    393                                  stdout=devnull, stderr=devnull,
    394                                  preexec_fn=setsid)
    395         except OSError:
    396             return False
    397         else:
    398             return (p.poll() is None)
    399 
    400 
    401 class Grail(BaseBrowser):
    402     # There should be a way to maintain a connection to Grail, but the
    403     # Grail remote control protocol doesn't really allow that at this
    404     # point.  It probably never will!
    405     def _find_grail_rc(self):
    406         import glob
    407         import pwd
    408         import socket
    409         import tempfile
    410         tempdir = os.path.join(tempfile.gettempdir(),
    411                                ".grail-unix")
    412         user = pwd.getpwuid(os.getuid())[0]
    413         filename = os.path.join(tempdir, user + "-*")
    414         maybes = glob.glob(filename)
    415         if not maybes:
    416             return None
    417         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    418         for fn in maybes:
    419             # need to PING each one until we find one that's live
    420             try:
    421                 s.connect(fn)
    422             except socket.error:
    423                 # no good; attempt to clean it out, but don't fail:
    424                 try:
    425                     os.unlink(fn)
    426                 except IOError:
    427                     pass
    428             else:
    429                 return s
    430 
    431     def _remote(self, action):
    432         s = self._find_grail_rc()
    433         if not s:
    434             return 0
    435         s.send(action)
    436         s.close()
    437         return 1
    438 
    439     def open(self, url, new=0, autoraise=True):
    440         if new:
    441             ok = self._remote("LOADNEW " + url)
    442         else:
    443             ok = self._remote("LOAD " + url)
    444         return ok
    445 
    446 
    447 #
    448 # Platform support for Unix
    449 #
    450 
    451 # These are the right tests because all these Unix browsers require either
    452 # a console terminal or an X display to run.
    453 
    454 def register_X_browsers():
    455 
    456     # use xdg-open if around
    457     if _iscommand("xdg-open"):
    458         register("xdg-open", None, BackgroundBrowser("xdg-open"))
    459 
    460     # The default GNOME3 browser
    461     if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gvfs-open"):
    462         register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
    463 
    464     # The default GNOME browser
    465     if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"):
    466         register("gnome-open", None, BackgroundBrowser("gnome-open"))
    467 
    468     # The default KDE browser
    469     if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"):
    470         register("kfmclient", Konqueror, Konqueror("kfmclient"))
    471 
    472     if _iscommand("x-www-browser"):
    473         register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
    474 
    475     # The Mozilla/Netscape browsers
    476     for browser in ("mozilla-firefox", "firefox",
    477                     "mozilla-firebird", "firebird",
    478                     "iceweasel", "iceape",
    479                     "seamonkey", "mozilla", "netscape"):
    480         if _iscommand(browser):
    481             register(browser, None, Mozilla(browser))
    482 
    483     # Konqueror/kfm, the KDE browser.
    484     if _iscommand("kfm"):
    485         register("kfm", Konqueror, Konqueror("kfm"))
    486     elif _iscommand("konqueror"):
    487         register("konqueror", Konqueror, Konqueror("konqueror"))
    488 
    489     # Gnome's Galeon and Epiphany
    490     for browser in ("galeon", "epiphany"):
    491         if _iscommand(browser):
    492             register(browser, None, Galeon(browser))
    493 
    494     # Skipstone, another Gtk/Mozilla based browser
    495     if _iscommand("skipstone"):
    496         register("skipstone", None, BackgroundBrowser("skipstone"))
    497 
    498     # Google Chrome/Chromium browsers
    499     for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):
    500         if _iscommand(browser):
    501             register(browser, None, Chrome(browser))
    502 
    503     # Opera, quite popular
    504     if _iscommand("opera"):
    505         register("opera", None, Opera("opera"))
    506 
    507     # Next, Mosaic -- old but still in use.
    508     if _iscommand("mosaic"):
    509         register("mosaic", None, BackgroundBrowser("mosaic"))
    510 
    511     # Grail, the Python browser. Does anybody still use it?
    512     if _iscommand("grail"):
    513         register("grail", Grail, None)
    514 
    515 # Prefer X browsers if present
    516 if os.environ.get("DISPLAY"):
    517     register_X_browsers()
    518 
    519 # Also try console browsers
    520 if os.environ.get("TERM"):
    521     if _iscommand("www-browser"):
    522         register("www-browser", None, GenericBrowser("www-browser"))
    523     # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
    524     if _iscommand("links"):
    525         register("links", None, GenericBrowser("links"))
    526     if _iscommand("elinks"):
    527         register("elinks", None, Elinks("elinks"))
    528     # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
    529     if _iscommand("lynx"):
    530         register("lynx", None, GenericBrowser("lynx"))
    531     # The w3m browser <http://w3m.sourceforge.net/>
    532     if _iscommand("w3m"):
    533         register("w3m", None, GenericBrowser("w3m"))
    534 
    535 #
    536 # Platform support for Windows
    537 #
    538 
    539 if sys.platform[:3] == "win":
    540     class WindowsDefault(BaseBrowser):
    541         def open(self, url, new=0, autoraise=True):
    542             try:
    543                 os.startfile(url)
    544             except WindowsError:
    545                 # [Error 22] No application is associated with the specified
    546                 # file for this operation: '<URL>'
    547                 return False
    548             else:
    549                 return True
    550 
    551     _tryorder = []
    552     _browsers = {}
    553 
    554     # First try to use the default Windows browser
    555     register("windows-default", WindowsDefault)
    556 
    557     # Detect some common Windows browsers, fallback to IE
    558     iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
    559                             "Internet Explorer\\IEXPLORE.EXE")
    560     for browser in ("firefox", "firebird", "seamonkey", "mozilla",
    561                     "netscape", "opera", iexplore):
    562         if _iscommand(browser):
    563             register(browser, None, BackgroundBrowser(browser))
    564 
    565 #
    566 # Platform support for MacOS
    567 #
    568 
    569 if sys.platform == 'darwin':
    570     # Adapted from patch submitted to SourceForge by Steven J. Burr
    571     class MacOSX(BaseBrowser):
    572         """Launcher class for Aqua browsers on Mac OS X
    573 
    574         Optionally specify a browser name on instantiation.  Note that this
    575         will not work for Aqua browsers if the user has moved the application
    576         package after installation.
    577 
    578         If no browser is specified, the default browser, as specified in the
    579         Internet System Preferences panel, will be used.
    580         """
    581         def __init__(self, name):
    582             self.name = name
    583 
    584         def open(self, url, new=0, autoraise=True):
    585             assert "'" not in url
    586             # hack for local urls
    587             if not ':' in url:
    588                 url = 'file:'+url
    589 
    590             # new must be 0 or 1
    591             new = int(bool(new))
    592             if self.name == "default":
    593                 # User called open, open_new or get without a browser parameter
    594                 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
    595             else:
    596                 # User called get and chose a browser
    597                 if self.name == "OmniWeb":
    598                     toWindow = ""
    599                 else:
    600                     # Include toWindow parameter of OpenURL command for browsers
    601                     # that support it.  0 == new window; -1 == existing
    602                     toWindow = "toWindow %d" % (new - 1)
    603                 cmd = 'OpenURL "%s"' % url.replace('"', '%22')
    604                 script = '''tell application "%s"
    605                                 activate
    606                                 %s %s
    607                             end tell''' % (self.name, cmd, toWindow)
    608             # Open pipe to AppleScript through osascript command
    609             osapipe = os.popen("osascript", "w")
    610             if osapipe is None:
    611                 return False
    612             # Write script to osascript's stdin
    613             osapipe.write(script)
    614             rc = osapipe.close()
    615             return not rc
    616 
    617     class MacOSXOSAScript(BaseBrowser):
    618         def __init__(self, name):
    619             self._name = name
    620 
    621         def open(self, url, new=0, autoraise=True):
    622             if self._name == 'default':
    623                 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
    624             else:
    625                 script = '''
    626                    tell application "%s"
    627                        activate
    628                        open location "%s"
    629                    end
    630                    '''%(self._name, url.replace('"', '%22'))
    631 
    632             osapipe = os.popen("osascript", "w")
    633             if osapipe is None:
    634                 return False
    635 
    636             osapipe.write(script)
    637             rc = osapipe.close()
    638             return not rc
    639 
    640 
    641     # Don't clear _tryorder or _browsers since OS X can use above Unix support
    642     # (but we prefer using the OS X specific stuff)
    643     register("safari", None, MacOSXOSAScript('safari'), -1)
    644     register("firefox", None, MacOSXOSAScript('firefox'), -1)
    645     register("MacOSX", None, MacOSXOSAScript('default'), -1)
    646 
    647 
    648 #
    649 # Platform support for OS/2
    650 #
    651 
    652 if sys.platform[:3] == "os2" and _iscommand("netscape"):
    653     _tryorder = []
    654     _browsers = {}
    655     register("os2netscape", None,
    656              GenericBrowser(["start", "netscape", "%s"]), -1)
    657 
    658 
    659 # OK, now that we know what the default preference orders for each
    660 # platform are, allow user to override them with the BROWSER variable.
    661 if "BROWSER" in os.environ:
    662     _userchoices = os.environ["BROWSER"].split(os.pathsep)
    663     _userchoices.reverse()
    664 
    665     # Treat choices in same way as if passed into get() but do register
    666     # and prepend to _tryorder
    667     for cmdline in _userchoices:
    668         if cmdline != '':
    669             cmd = _synthesize(cmdline, -1)
    670             if cmd[1] is None:
    671                 register(cmdline, None, GenericBrowser(cmdline), -1)
    672     cmdline = None # to make del work if _userchoices was empty
    673     del cmdline
    674     del _userchoices
    675 
    676 # what to do if _tryorder is now empty?
    677 
    678 
    679 def main():
    680     import getopt
    681     usage = """Usage: %s [-n | -t] url
    682     -n: open new window
    683     -t: open new tab""" % sys.argv[0]
    684     try:
    685         opts, args = getopt.getopt(sys.argv[1:], 'ntd')
    686     except getopt.error, msg:
    687         print >>sys.stderr, msg
    688         print >>sys.stderr, usage
    689         sys.exit(1)
    690     new_win = 0
    691     for o, a in opts:
    692         if o == '-n': new_win = 1
    693         elif o == '-t': new_win = 2
    694     if len(args) != 1:
    695         print >>sys.stderr, usage
    696         sys.exit(1)
    697 
    698     url = args[0]
    699     open(url, new_win)
    700 
    701     print "\a"
    702 
    703 if __name__ == "__main__":
    704     main()
    705