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