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