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