source: trunk/essentials/dev-lang/python/Lib/webbrowser.py@ 3226

Last change on this file since 3226 was 3225, checked in by bird, 19 years ago

Python 2.5

File size: 19.7 KB
Line 
1#! /usr/bin/env python
2"""Interfaces for launching and remotely controlling Web browsers."""
3
4import os
5import sys
6import stat
7import subprocess
8import time
9
10__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
11
12class Error(Exception):
13 pass
14
15_browsers = {} # Dictionary of available browser controllers
16_tryorder = [] # Preference order of available browsers
17
18def 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)
25
26def 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")
47
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 *".
51
52def 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
58
59def open_new(url):
60 return open(url, 1)
61
62def open_new_tab(url):
63 return open(url, 2)
64
65
66def _synthesize(browser, update_tryorder=1):
67 """Attempt to synthesize a controller base on existing controllers.
68
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.
73
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].
76
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]
96
97
98if sys.platform[:3] == "win":
99 def _isexecutable(cmd):
100 cmd = cmd.lower()
101 if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
102 return True
103 for ext in ".exe", ".bat":
104 if os.path.isfile(cmd + ext):
105 return True
106 return False
107else:
108 def _isexecutable(cmd):
109 if os.path.isfile(cmd):
110 mode = os.stat(cmd)[stat.ST_MODE]
111 if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
112 return True
113 return False
114
115def _iscommand(cmd):
116 """Return True if cmd is executable or can be found on the executable
117 search path."""
118 if _isexecutable(cmd):
119 return True
120 path = os.environ.get("PATH")
121 if not path:
122 return False
123 for d in path.split(os.pathsep):
124 exe = os.path.join(d, cmd)
125 if _isexecutable(exe):
126 return True
127 return False
128
129
130# General parent classes
131
132class BaseBrowser(object):
133 """Parent class for all browsers. Do not use directly."""
134
135 args = ['%s']
136
137 def __init__(self, name=""):
138 self.name = name
139 self.basename = name
140
141 def open(self, url, new=0, autoraise=1):
142 raise NotImplementedError
143
144 def open_new(self, url):
145 return self.open(url, 1)
146
147 def open_new_tab(self, url):
148 return self.open(url, 2)
149
150
151class GenericBrowser(BaseBrowser):
152 """Class for all browsers started with a command
153 and without remote functionality."""
154
155 def __init__(self, name):
156 if isinstance(name, basestring):
157 self.name = name
158 else:
159 # name should be a list with arguments
160 self.name = name[0]
161 self.args = name[1:]
162 self.basename = os.path.basename(self.name)
163
164 def open(self, url, new=0, autoraise=1):
165 cmdline = [self.name] + [arg.replace("%s", url)
166 for arg in self.args]
167 try:
168 p = subprocess.Popen(cmdline, close_fds=True)
169 return not p.wait()
170 except OSError:
171 return False
172
173
174class BackgroundBrowser(GenericBrowser):
175 """Class for all browsers which are to be started in the
176 background."""
177
178 def open(self, url, new=0, autoraise=1):
179 cmdline = [self.name] + [arg.replace("%s", url)
180 for arg in self.args]
181 setsid = getattr(os, 'setsid', None)
182 if not setsid:
183 setsid = getattr(os, 'setpgrp', None)
184 try:
185 p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
186 return (p.poll() is None)
187 except OSError:
188 return False
189
190
191class UnixBrowser(BaseBrowser):
192 """Parent class for all Unix browsers with remote functionality."""
193
194 raise_opts = None
195 remote_args = ['%action', '%s']
196 remote_action = None
197 remote_action_newwin = None
198 remote_action_newtab = None
199 background = False
200 redirect_stdout = True
201
202 def _invoke(self, args, remote, autoraise):
203 raise_opt = []
204 if remote and self.raise_opts:
205 # use autoraise argument only for remote invocation
206 autoraise = int(bool(autoraise))
207 opt = self.raise_opts[autoraise]
208 if opt: raise_opt = [opt]
209
210 cmdline = [self.name] + raise_opt + args
211
212 if remote or self.background:
213 inout = file(os.devnull, "r+")
214 else:
215 # for TTY browsers, we need stdin/out
216 inout = None
217 # if possible, put browser in separate process group, so
218 # keyboard interrupts don't affect browser as well as Python
219 setsid = getattr(os, 'setsid', None)
220 if not setsid:
221 setsid = getattr(os, 'setpgrp', None)
222
223 p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
224 stdout=(self.redirect_stdout and inout or None),
225 stderr=inout, preexec_fn=setsid)
226 if remote:
227 # wait five secons. If the subprocess is not finished, the
228 # remote invocation has (hopefully) started a new instance.
229 time.sleep(1)
230 rc = p.poll()
231 if rc is None:
232 time.sleep(4)
233 rc = p.poll()
234 if rc is None:
235 return True
236 # if remote call failed, open() will try direct invocation
237 return not rc
238 elif self.background:
239 if p.poll() is None:
240 return True
241 else:
242 return False
243 else:
244 return not p.wait()
245
246 def open(self, url, new=0, autoraise=1):
247 if new == 0:
248 action = self.remote_action
249 elif new == 1:
250 action = self.remote_action_newwin
251 elif new == 2:
252 if self.remote_action_newtab is None:
253 action = self.remote_action_newwin
254 else:
255 action = self.remote_action_newtab
256 else:
257 raise Error("Bad 'new' parameter to open(); " +
258 "expected 0, 1, or 2, got %s" % new)
259
260 args = [arg.replace("%s", url).replace("%action", action)
261 for arg in self.remote_args]
262 success = self._invoke(args, True, autoraise)
263 if not success:
264 # remote invocation failed, try straight way
265 args = [arg.replace("%s", url) for arg in self.args]
266 return self._invoke(args, False, False)
267 else:
268 return True
269
270
271class Mozilla(UnixBrowser):
272 """Launcher class for Mozilla/Netscape browsers."""
273
274 raise_opts = ["-noraise", "-raise"]
275
276 remote_args = ['-remote', 'openURL(%s%action)']
277 remote_action = ""
278 remote_action_newwin = ",new-window"
279 remote_action_newtab = ",new-tab"
280
281 background = True
282
283Netscape = Mozilla
284
285
286class Galeon(UnixBrowser):
287 """Launcher class for Galeon/Epiphany browsers."""
288
289 raise_opts = ["-noraise", ""]
290 remote_args = ['%action', '%s']
291 remote_action = "-n"
292 remote_action_newwin = "-w"
293
294 background = True
295
296
297class Opera(UnixBrowser):
298 "Launcher class for Opera browser."
299
300 raise_opts = ["", "-raise"]
301
302 remote_args = ['-remote', 'openURL(%s%action)']
303 remote_action = ""
304 remote_action_newwin = ",new-window"
305 remote_action_newtab = ",new-page"
306 background = True
307
308
309class Elinks(UnixBrowser):
310 "Launcher class for Elinks browsers."
311
312 remote_args = ['-remote', 'openURL(%s%action)']
313 remote_action = ""
314 remote_action_newwin = ",new-window"
315 remote_action_newtab = ",new-tab"
316 background = False
317
318 # elinks doesn't like its stdout to be redirected -
319 # it uses redirected stdout as a signal to do -dump
320 redirect_stdout = False
321
322
323class Konqueror(BaseBrowser):
324 """Controller for the KDE File Manager (kfm, or Konqueror).
325
326 See the output of ``kfmclient --commands``
327 for more information on the Konqueror remote-control interface.
328 """
329
330 def open(self, url, new=0, autoraise=1):
331 # XXX Currently I know no way to prevent KFM from opening a new win.
332 if new == 2:
333 action = "newTab"
334 else:
335 action = "openURL"
336
337 devnull = file(os.devnull, "r+")
338 # if possible, put browser in separate process group, so
339 # keyboard interrupts don't affect browser as well as Python
340 setsid = getattr(os, 'setsid', None)
341 if not setsid:
342 setsid = getattr(os, 'setpgrp', None)
343
344 try:
345 p = subprocess.Popen(["kfmclient", action, url],
346 close_fds=True, stdin=devnull,
347 stdout=devnull, stderr=devnull)
348 except OSError:
349 # fall through to next variant
350 pass
351 else:
352 p.wait()
353 # kfmclient's return code unfortunately has no meaning as it seems
354 return True
355
356 try:
357 p = subprocess.Popen(["konqueror", "--silent", url],
358 close_fds=True, stdin=devnull,
359 stdout=devnull, stderr=devnull,
360 preexec_fn=setsid)
361 except OSError:
362 # fall through to next variant
363 pass
364 else:
365 if p.poll() is None:
366 # Should be running now.
367 return True
368
369 try:
370 p = subprocess.Popen(["kfm", "-d", url],
371 close_fds=True, stdin=devnull,
372 stdout=devnull, stderr=devnull,
373 preexec_fn=setsid)
374 except OSError:
375 return False
376 else:
377 return (p.poll() is None)
378
379
380class Grail(BaseBrowser):
381 # There should be a way to maintain a connection to Grail, but the
382 # Grail remote control protocol doesn't really allow that at this
383 # point. It probably never will!
384 def _find_grail_rc(self):
385 import glob
386 import pwd
387 import socket
388 import tempfile
389 tempdir = os.path.join(tempfile.gettempdir(),
390 ".grail-unix")
391 user = pwd.getpwuid(os.getuid())[0]
392 filename = os.path.join(tempdir, user + "-*")
393 maybes = glob.glob(filename)
394 if not maybes:
395 return None
396 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
397 for fn in maybes:
398 # need to PING each one until we find one that's live
399 try:
400 s.connect(fn)
401 except socket.error:
402 # no good; attempt to clean it out, but don't fail:
403 try:
404 os.unlink(fn)
405 except IOError:
406 pass
407 else:
408 return s
409
410 def _remote(self, action):
411 s = self._find_grail_rc()
412 if not s:
413 return 0
414 s.send(action)
415 s.close()
416 return 1
417
418 def open(self, url, new=0, autoraise=1):
419 if new:
420 ok = self._remote("LOADNEW " + url)
421 else:
422 ok = self._remote("LOAD " + url)
423 return ok
424
425
426#
427# Platform support for Unix
428#
429
430# These are the right tests because all these Unix browsers require either
431# a console terminal or an X display to run.
432
433def register_X_browsers():
434 # The default Gnome browser
435 if _iscommand("gconftool-2"):
436 # get the web browser string from gconftool
437 gc = 'gconftool-2 -g /desktop/gnome/url-handlers/http/command 2>/dev/null'
438 out = os.popen(gc)
439 commd = out.read().strip()
440 retncode = out.close()
441
442 # if successful, register it
443 if retncode is None and commd:
444 register("gnome", None, BackgroundBrowser(commd))
445
446 # First, the Mozilla/Netscape browsers
447 for browser in ("mozilla-firefox", "firefox",
448 "mozilla-firebird", "firebird",
449 "seamonkey", "mozilla", "netscape"):
450 if _iscommand(browser):
451 register(browser, None, Mozilla(browser))
452
453 # Konqueror/kfm, the KDE browser.
454 if _iscommand("kfm"):
455 register("kfm", Konqueror, Konqueror("kfm"))
456 elif _iscommand("konqueror"):
457 register("konqueror", Konqueror, Konqueror("konqueror"))
458
459 # Gnome's Galeon and Epiphany
460 for browser in ("galeon", "epiphany"):
461 if _iscommand(browser):
462 register(browser, None, Galeon(browser))
463
464 # Skipstone, another Gtk/Mozilla based browser
465 if _iscommand("skipstone"):
466 register("skipstone", None, BackgroundBrowser("skipstone"))
467
468 # Opera, quite popular
469 if _iscommand("opera"):
470 register("opera", None, Opera("opera"))
471
472 # Next, Mosaic -- old but still in use.
473 if _iscommand("mosaic"):
474 register("mosaic", None, BackgroundBrowser("mosaic"))
475
476 # Grail, the Python browser. Does anybody still use it?
477 if _iscommand("grail"):
478 register("grail", Grail, None)
479
480# Prefer X browsers if present
481if os.environ.get("DISPLAY"):
482 register_X_browsers()
483
484# Also try console browsers
485if os.environ.get("TERM"):
486 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
487 if _iscommand("links"):
488 register("links", None, GenericBrowser("links"))
489 if _iscommand("elinks"):
490 register("elinks", None, Elinks("elinks"))
491 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
492 if _iscommand("lynx"):
493 register("lynx", None, GenericBrowser("lynx"))
494 # The w3m browser <http://w3m.sourceforge.net/>
495 if _iscommand("w3m"):
496 register("w3m", None, GenericBrowser("w3m"))
497
498#
499# Platform support for Windows
500#
501
502if sys.platform[:3] == "win":
503 class WindowsDefault(BaseBrowser):
504 def open(self, url, new=0, autoraise=1):
505 os.startfile(url)
506 return True # Oh, my...
507
508 _tryorder = []
509 _browsers = {}
510 # Prefer mozilla/netscape/opera if present
511 for browser in ("firefox", "firebird", "seamonkey", "mozilla",
512 "netscape", "opera"):
513 if _iscommand(browser):