• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python
2"""Interfaces for launching and remotely controlling Web browsers."""
3# Maintained by Georg Brandl.
4
5import os
6import shlex
7import sys
8import stat
9import subprocess
10import time
11
12__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
13
14class Error(Exception):
15    pass
16
17_browsers = {}          # Dictionary of available browser controllers
18_tryorder = []          # Preference order of available browsers
19
20def 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
28def 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
58def 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
65def open_new(url):
66    return open(url, 1)
67
68def open_new_tab(url):
69    return open(url, 2)
70
71
72def _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
104if 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
113else:
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
121def _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
138class 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
157class 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
184class 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
204class 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
284class 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
294Netscape = Mozilla
295
296
297class 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
307class 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
316Chromium = Chrome
317
318
319class Opera(UnixBrowser):
320    "Launcher class for Opera browser."
321
322    remote_args = ['%action', '%s']
323    remote_action = ""
324    remote_action_newwin = "--new-window"
325    remote_action_newtab = ""
326    background = True
327
328
329class Elinks(UnixBrowser):
330    "Launcher class for Elinks browsers."
331
332    remote_args = ['-remote', 'openURL(%s%action)']
333    remote_action = ""
334    remote_action_newwin = ",new-window"
335    remote_action_newtab = ",new-tab"
336    background = False
337
338    # elinks doesn't like its stdout to be redirected -
339    # it uses redirected stdout as a signal to do -dump
340    redirect_stdout = False
341
342
343class Konqueror(BaseBrowser):
344    """Controller for the KDE File Manager (kfm, or Konqueror).
345
346    See the output of ``kfmclient --commands``
347    for more information on the Konqueror remote-control interface.
348    """
349
350    def open(self, url, new=0, autoraise=True):
351        # XXX Currently I know no way to prevent KFM from opening a new win.
352        if new == 2:
353            action = "newTab"
354        else:
355            action = "openURL"
356
357        devnull = file(os.devnull, "r+")
358        # if possible, put browser in separate process group, so
359        # keyboard interrupts don't affect browser as well as Python
360        setsid = getattr(os, 'setsid', None)
361        if not setsid:
362            setsid = getattr(os, 'setpgrp', None)
363
364        try:
365            p = subprocess.Popen(["kfmclient", action, url],
366                                 close_fds=True, stdin=devnull,
367                                 stdout=devnull, stderr=devnull)
368        except OSError:
369            # fall through to next variant
370            pass
371        else:
372            p.wait()
373            # kfmclient's return code unfortunately has no meaning as it seems
374            return True
375
376        try:
377            p = subprocess.Popen(["konqueror", "--silent", url],
378                                 close_fds=True, stdin=devnull,
379                                 stdout=devnull, stderr=devnull,
380                                 preexec_fn=setsid)
381        except OSError:
382            # fall through to next variant
383            pass
384        else:
385            if p.poll() is None:
386                # Should be running now.
387                return True
388
389        try:
390            p = subprocess.Popen(["kfm", "-d", url],
391                                 close_fds=True, stdin=devnull,
392                                 stdout=devnull, stderr=devnull,
393                                 preexec_fn=setsid)
394        except OSError:
395            return False
396        else:
397            return (p.poll() is None)
398
399
400class Grail(BaseBrowser):
401    # There should be a way to maintain a connection to Grail, but the
402    # Grail remote control protocol doesn't really allow that at this
403    # point.  It probably never will!
404    def _find_grail_rc(self):
405        import glob
406        import pwd
407        import socket
408        import tempfile
409        tempdir = os.path.join(tempfile.gettempdir(),
410                               ".grail-unix")
411        user = pwd.getpwuid(os.getuid())[0]
412        filename = os.path.join(tempdir, user + "-*")
413        maybes = glob.glob(filename)
414        if not maybes:
415            return None
416        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
417        for fn in maybes:
418            # need to PING each one until we find one that's live
419            try:
420                s.connect(fn)
421            except socket.error:
422                # no good; attempt to clean it out, but don't fail:
423                try:
424                    os.unlink(fn)
425                except IOError:
426                    pass
427            else:
428                return s
429
430    def _remote(self, action):
431        s = self._find_grail_rc()
432        if not s:
433            return 0
434        s.send(action)
435        s.close()
436        return 1
437
438    def open(self, url, new=0, autoraise=True):
439        if new:
440            ok = self._remote("LOADNEW " + url)
441        else:
442            ok = self._remote("LOAD " + url)
443        return ok
444
445
446#
447# Platform support for Unix
448#
449
450# These are the right tests because all these Unix browsers require either
451# a console terminal or an X display to run.
452
453def register_X_browsers():
454
455    # use xdg-open if around
456    if _iscommand("xdg-open"):
457        register("xdg-open", None, BackgroundBrowser("xdg-open"))
458
459    # The default GNOME3 browser
460    if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gvfs-open"):
461        register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
462
463    # The default GNOME browser
464    if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"):
465        register("gnome-open", None, BackgroundBrowser("gnome-open"))
466
467    # The default KDE browser
468    if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"):
469        register("kfmclient", Konqueror, Konqueror("kfmclient"))
470
471    if _iscommand("x-www-browser"):
472        register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
473
474    # The Mozilla/Netscape browsers
475    for browser in ("mozilla-firefox", "firefox",
476                    "mozilla-firebird", "firebird",
477                    "iceweasel", "iceape",
478                    "seamonkey", "mozilla", "netscape"):
479        if _iscommand(browser):
480            register(browser, None, Mozilla(browser))
481
482    # Konqueror/kfm, the KDE browser.
483    if _iscommand("kfm"):
484        register("kfm", Konqueror, Konqueror("kfm"))
485    elif _iscommand("konqueror"):
486        register("konqueror", Konqueror, Konqueror("konqueror"))
487
488    # Gnome's Galeon and Epiphany
489    for browser in ("galeon", "epiphany"):
490        if _iscommand(browser):
491            register(browser, None, Galeon(browser))
492
493    # Skipstone, another Gtk/Mozilla based browser
494    if _iscommand("skipstone"):
495        register("skipstone", None, BackgroundBrowser("skipstone"))
496
497    # Google Chrome/Chromium browsers
498    for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):
499        if _iscommand(browser):
500            register(browser, None, Chrome(browser))
501
502    # Opera, quite popular
503    if _iscommand("opera"):
504        register("opera", None, Opera("opera"))
505
506    # Next, Mosaic -- old but still in use.
507    if _iscommand("mosaic"):
508        register("mosaic", None, BackgroundBrowser("mosaic"))
509
510    # Grail, the Python browser. Does anybody still use it?
511    if _iscommand("grail"):
512        register("grail", Grail, None)
513
514# Prefer X browsers if present
515if os.environ.get("DISPLAY"):
516    register_X_browsers()
517
518# Also try console browsers
519if os.environ.get("TERM"):
520    if _iscommand("www-browser"):
521        register("www-browser", None, GenericBrowser("www-browser"))
522    # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
523    if _iscommand("links"):
524        register("links", None, GenericBrowser("links"))
525    if _iscommand("elinks"):
526        register("elinks", None, Elinks("elinks"))
527    # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
528    if _iscommand("lynx"):
529        register("lynx", None, GenericBrowser("lynx"))
530    # The w3m browser <http://w3m.sourceforge.net/>
531    if _iscommand("w3m"):
532        register("w3m", None, GenericBrowser("w3m"))
533
534#
535# Platform support for Windows
536#
537
538if sys.platform[:3] == "win":
539    class WindowsDefault(BaseBrowser):
540        def open(self, url, new=0, autoraise=True):
541            try:
542                os.startfile(url)
543            except WindowsError:
544                # [Error 22] No application is associated with the specified
545                # file for this operation: '<URL>'
546                return False
547            else:
548                return True
549
550    _tryorder = []
551    _browsers = {}
552
553    # First try to use the default Windows browser
554    register("windows-default", WindowsDefault)
555
556    # Detect some common Windows browsers, fallback to IE
557    iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
558                            "Internet Explorer\\IEXPLORE.EXE")
559    for browser in ("firefox", "firebird", "seamonkey", "mozilla",
560                    "netscape", "opera", iexplore):
561        if _iscommand(browser):
562            register(browser, None, BackgroundBrowser(browser))
563
564#
565# Platform support for MacOS
566#
567
568if sys.platform == 'darwin':
569    # Adapted from patch submitted to SourceForge by Steven J. Burr
570    class MacOSX(BaseBrowser):
571        """Launcher class for Aqua browsers on Mac OS X
572
573        Optionally specify a browser name on instantiation.  Note that this
574        will not work for Aqua browsers if the user has moved the application
575        package after installation.
576
577        If no browser is specified, the default browser, as specified in the
578        Internet System Preferences panel, will be used.
579        """
580        def __init__(self, name):
581            self.name = name
582
583        def open(self, url, new=0, autoraise=True):
584            assert "'" not in url
585            # hack for local urls
586            if not ':' in url:
587                url = 'file:'+url
588
589            # new must be 0 or 1
590            new = int(bool(new))
591            if self.name == "default":
592                # User called open, open_new or get without a browser parameter
593                script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
594            else:
595                # User called get and chose a browser
596                if self.name == "OmniWeb":
597                    toWindow = ""
598                else:
599                    # Include toWindow parameter of OpenURL command for browsers
600                    # that support it.  0 == new window; -1 == existing
601                    toWindow = "toWindow %d" % (new - 1)
602                cmd = 'OpenURL "%s"' % url.replace('"', '%22')
603                script = '''tell application "%s"
604                                activate
605                                %s %s
606                            end tell''' % (self.name, cmd, toWindow)
607            # Open pipe to AppleScript through osascript command
608            osapipe = os.popen("osascript", "w")
609            if osapipe is None:
610                return False
611            # Write script to osascript's stdin
612            osapipe.write(script)
613            rc = osapipe.close()
614            return not rc
615
616    class MacOSXOSAScript(BaseBrowser):
617        def __init__(self, name):
618            self._name = name
619
620        def open(self, url, new=0, autoraise=True):
621            if self._name == 'default':
622                script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
623            else:
624                script = '''
625                   tell application "%s"
626                       activate
627                       open location "%s"
628                   end
629                   '''%(self._name, url.replace('"', '%22'))
630
631            osapipe = os.popen("osascript", "w")
632            if osapipe is None:
633                return False
634
635            osapipe.write(script)
636            rc = osapipe.close()
637            return not rc
638
639
640    # Don't clear _tryorder or _browsers since OS X can use above Unix support
641    # (but we prefer using the OS X specific stuff)
642    register("safari", None, MacOSXOSAScript('safari'), -1)
643    register("firefox", None, MacOSXOSAScript('firefox'), -1)
644    register("chrome", None, MacOSXOSAScript('chrome'), -1)
645    register("MacOSX", None, MacOSXOSAScript('default'), -1)
646
647
648#
649# Platform support for OS/2
650#
651
652if 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.
661if "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
679def 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
703if __name__ == "__main__":
704    main()
705