| Ka-Ping Yee | 0a8c29b | 2001-03-02 02:01:40 +0000 | [diff] [blame] | 1 | """Interfaces for launching and remotely controlling Web browsers.""" | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 2 |  | 
 | 3 | import os | 
 | 4 | import sys | 
 | 5 |  | 
| Skip Montanaro | 40fc160 | 2001-03-01 04:27:19 +0000 | [diff] [blame] | 6 | __all__ = ["Error", "open", "get", "register"] | 
 | 7 |  | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 8 | class Error(Exception): | 
 | 9 |     pass | 
 | 10 |  | 
| Tim Peters | 658cba6 | 2001-02-09 20:06:00 +0000 | [diff] [blame] | 11 | _browsers = {}          # Dictionary of available browser controllers | 
 | 12 | _tryorder = []          # Preference order of available browsers | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 13 |  | 
 | 14 | def register(name, klass, instance=None): | 
 | 15 |     """Register a browser connector and, optionally, connection.""" | 
 | 16 |     _browsers[name.lower()] = [klass, instance] | 
 | 17 |  | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 18 | def get(using=None): | 
 | 19 |     """Return a browser launcher instance appropriate for the environment.""" | 
 | 20 |     if using: | 
 | 21 |         alternatives = [using] | 
 | 22 |     else: | 
 | 23 |         alternatives = _tryorder | 
 | 24 |     for browser in alternatives: | 
 | 25 |         if browser.find('%s') > -1: | 
 | 26 |             # User gave us a command line, don't mess with it. | 
| Eric S. Raymond | f7eb4fa | 2001-03-31 01:50:52 +0000 | [diff] [blame] | 27 |             return GenericBrowser(browser) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 28 |         else: | 
| Tim Peters | 658cba6 | 2001-02-09 20:06:00 +0000 | [diff] [blame] | 29 |             # User gave us a browser name. | 
| Fred Drake | f4e5bd9 | 2001-04-12 22:07:27 +0000 | [diff] [blame] | 30 |             try: | 
 | 31 |                 command = _browsers[browser.lower()] | 
 | 32 |             except KeyError: | 
 | 33 |                 command = _synthesize(browser) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 34 |             if command[1] is None: | 
 | 35 |                 return command[0]() | 
 | 36 |             else: | 
 | 37 |                 return command[1] | 
 | 38 |     raise Error("could not locate runnable browser") | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 39 |  | 
 | 40 | # Please note: the following definition hides a builtin function. | 
 | 41 |  | 
| Eric S. Raymond | f79cb2d | 2001-01-23 13:49:44 +0000 | [diff] [blame] | 42 | def open(url, new=0, autoraise=1): | 
 | 43 |     get().open(url, new, autoraise) | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 44 |  | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 45 | def open_new(url): | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 46 |     get().open(url, 1) | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 47 |  | 
| Fred Drake | f4e5bd9 | 2001-04-12 22:07:27 +0000 | [diff] [blame] | 48 |  | 
 | 49 | def _synthesize(browser): | 
 | 50 |     """Attempt to synthesize a controller base on existing controllers. | 
 | 51 |  | 
 | 52 |     This is useful to create a controller when a user specifies a path to | 
 | 53 |     an entry in the BROWSER environment variable -- we can copy a general | 
 | 54 |     controller to operate using a specific installation of the desired | 
 | 55 |     browser in this way. | 
 | 56 |  | 
 | 57 |     If we can't create a controller in this way, or if there is no | 
 | 58 |     executable for the requested browser, return [None, None]. | 
 | 59 |  | 
 | 60 |     """ | 
 | 61 |     if not os.path.exists(browser): | 
 | 62 |         return [None, None] | 
 | 63 |     name = os.path.basename(browser) | 
 | 64 |     try: | 
 | 65 |         command = _browsers[name.lower()] | 
 | 66 |     except KeyError: | 
 | 67 |         return [None, None] | 
 | 68 |     # now attempt to clone to fit the new name: | 
 | 69 |     controller = command[1] | 
 | 70 |     if controller and name.lower() == controller.basename: | 
 | 71 |         import copy | 
 | 72 |         controller = copy.copy(controller) | 
 | 73 |         controller.name = browser | 
 | 74 |         controller.basename = os.path.basename(browser) | 
 | 75 |         register(browser, None, controller) | 
 | 76 |         return [None, controller] | 
| Andrew M. Kuchling | 118aa53 | 2001-08-13 14:37:23 +0000 | [diff] [blame] | 77 |     return [None, None] | 
| Fred Drake | f4e5bd9 | 2001-04-12 22:07:27 +0000 | [diff] [blame] | 78 |  | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 79 |  | 
 | 80 | def _iscommand(cmd): | 
| Tim Peters | bc0e910 | 2002-04-04 22:55:58 +0000 | [diff] [blame] | 81 |     """Return True if cmd can be found on the executable search path.""" | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 82 |     path = os.environ.get("PATH") | 
 | 83 |     if not path: | 
| Tim Peters | bc0e910 | 2002-04-04 22:55:58 +0000 | [diff] [blame] | 84 |         return False | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 85 |     for d in path.split(os.pathsep): | 
 | 86 |         exe = os.path.join(d, cmd) | 
 | 87 |         if os.path.isfile(exe): | 
| Tim Peters | bc0e910 | 2002-04-04 22:55:58 +0000 | [diff] [blame] | 88 |             return True | 
 | 89 |     return False | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 90 |  | 
 | 91 |  | 
 | 92 | PROCESS_CREATION_DELAY = 4 | 
 | 93 |  | 
 | 94 |  | 
 | 95 | class GenericBrowser: | 
 | 96 |     def __init__(self, cmd): | 
 | 97 |         self.name, self.args = cmd.split(None, 1) | 
 | 98 |         self.basename = os.path.basename(self.name) | 
 | 99 |  | 
 | 100 |     def open(self, url, new=0, autoraise=1): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 101 |         assert "'" not in url | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 102 |         command = "%s %s" % (self.name, self.args) | 
 | 103 |         os.system(command % url) | 
 | 104 |  | 
 | 105 |     def open_new(self, url): | 
 | 106 |         self.open(url) | 
 | 107 |  | 
 | 108 |  | 
 | 109 | class Netscape: | 
 | 110 |     "Launcher class for Netscape browsers." | 
 | 111 |     def __init__(self, name): | 
 | 112 |         self.name = name | 
 | 113 |         self.basename = os.path.basename(name) | 
 | 114 |  | 
 | 115 |     def _remote(self, action, autoraise): | 
 | 116 |         raise_opt = ("-noraise", "-raise")[autoraise] | 
 | 117 |         cmd = "%s %s -remote '%s' >/dev/null 2>&1" % (self.name, | 
 | 118 |                                                       raise_opt, | 
 | 119 |                                                       action) | 
 | 120 |         rc = os.system(cmd) | 
 | 121 |         if rc: | 
 | 122 |             import time | 
 | 123 |             os.system("%s &" % self.name) | 
 | 124 |             time.sleep(PROCESS_CREATION_DELAY) | 
 | 125 |             rc = os.system(cmd) | 
 | 126 |         return not rc | 
 | 127 |  | 
 | 128 |     def open(self, url, new=0, autoraise=1): | 
 | 129 |         if new: | 
 | 130 |             self._remote("openURL(%s, new-window)"%url, autoraise) | 
 | 131 |         else: | 
 | 132 |             self._remote("openURL(%s)" % url, autoraise) | 
 | 133 |  | 
 | 134 |     def open_new(self, url): | 
 | 135 |         self.open(url, 1) | 
 | 136 |  | 
 | 137 |  | 
 | 138 | class Konqueror: | 
 | 139 |     """Controller for the KDE File Manager (kfm, or Konqueror). | 
 | 140 |  | 
 | 141 |     See http://developer.kde.org/documentation/other/kfmclient.html | 
 | 142 |     for more information on the Konqueror remote-control interface. | 
 | 143 |  | 
 | 144 |     """ | 
 | 145 |     def __init__(self): | 
 | 146 |         if _iscommand("konqueror"): | 
 | 147 |             self.name = self.basename = "konqueror" | 
 | 148 |         else: | 
 | 149 |             self.name = self.basename = "kfm" | 
 | 150 |  | 
 | 151 |     def _remote(self, action): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 152 |         assert "'" not in action | 
 | 153 |         cmd = "kfmclient '%s' >/dev/null 2>&1" % action | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 154 |         rc = os.system(cmd) | 
 | 155 |         if rc: | 
 | 156 |             import time | 
 | 157 |             if self.basename == "konqueror": | 
 | 158 |                 os.system(self.name + " --silent &") | 
 | 159 |             else: | 
 | 160 |                 os.system(self.name + " -d &") | 
 | 161 |             time.sleep(PROCESS_CREATION_DELAY) | 
 | 162 |             rc = os.system(cmd) | 
 | 163 |         return not rc | 
 | 164 |  | 
 | 165 |     def open(self, url, new=1, autoraise=1): | 
 | 166 |         # XXX Currently I know no way to prevent KFM from | 
 | 167 |         # opening a new win. | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 168 |         self._remote("openURL '%s'" % url) | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 169 |  | 
 | 170 |     open_new = open | 
 | 171 |  | 
 | 172 |  | 
 | 173 | class Grail: | 
 | 174 |     # There should be a way to maintain a connection to Grail, but the | 
 | 175 |     # Grail remote control protocol doesn't really allow that at this | 
 | 176 |     # point.  It probably neverwill! | 
 | 177 |     def _find_grail_rc(self): | 
 | 178 |         import glob | 
 | 179 |         import pwd | 
 | 180 |         import socket | 
 | 181 |         import tempfile | 
 | 182 |         tempdir = os.path.join(tempfile.gettempdir(), | 
 | 183 |                                ".grail-unix") | 
| Fred Drake | 16623fe | 2001-10-13 16:00:52 +0000 | [diff] [blame] | 184 |         user = pwd.getpwuid(os.getuid())[0] | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 185 |         filename = os.path.join(tempdir, user + "-*") | 
 | 186 |         maybes = glob.glob(filename) | 
 | 187 |         if not maybes: | 
 | 188 |             return None | 
 | 189 |         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | 
 | 190 |         for fn in maybes: | 
 | 191 |             # need to PING each one until we find one that's live | 
 | 192 |             try: | 
 | 193 |                 s.connect(fn) | 
 | 194 |             except socket.error: | 
 | 195 |                 # no good; attempt to clean it out, but don't fail: | 
 | 196 |                 try: | 
 | 197 |                     os.unlink(fn) | 
 | 198 |                 except IOError: | 
 | 199 |                     pass | 
 | 200 |             else: | 
 | 201 |                 return s | 
 | 202 |  | 
 | 203 |     def _remote(self, action): | 
 | 204 |         s = self._find_grail_rc() | 
 | 205 |         if not s: | 
 | 206 |             return 0 | 
 | 207 |         s.send(action) | 
 | 208 |         s.close() | 
 | 209 |         return 1 | 
 | 210 |  | 
 | 211 |     def open(self, url, new=0, autoraise=1): | 
 | 212 |         if new: | 
 | 213 |             self._remote("LOADNEW " + url) | 
 | 214 |         else: | 
 | 215 |             self._remote("LOAD " + url) | 
 | 216 |  | 
 | 217 |     def open_new(self, url): | 
 | 218 |         self.open(url, 1) | 
 | 219 |  | 
 | 220 |  | 
 | 221 | class WindowsDefault: | 
 | 222 |     def open(self, url, new=0, autoraise=1): | 
 | 223 |         os.startfile(url) | 
 | 224 |  | 
 | 225 |     def open_new(self, url): | 
 | 226 |         self.open(url) | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 227 |  | 
| Tim Peters | 658cba6 | 2001-02-09 20:06:00 +0000 | [diff] [blame] | 228 | # | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 229 | # Platform support for Unix | 
 | 230 | # | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 231 |  | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 232 | # This is the right test because all these Unix browsers require either | 
 | 233 | # a console terminal of an X display to run.  Note that we cannot split | 
 | 234 | # the TERM and DISPLAY cases, because we might be running Python from inside | 
 | 235 | # an xterm. | 
 | 236 | if os.environ.get("TERM") or os.environ.get("DISPLAY"): | 
| Guido van Rossum | cb33165 | 2001-12-03 15:51:31 +0000 | [diff] [blame] | 237 |     _tryorder = ["mozilla","netscape","kfm","grail","links","lynx","w3m"] | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 238 |  | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 239 |     # Easy cases first -- register console browsers if we have them. | 
 | 240 |     if os.environ.get("TERM"): | 
 | 241 |         # The Links browser <http://artax.karlin.mff.cuni.cz/~mikulas/links/> | 
 | 242 |         if _iscommand("links"): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 243 |             register("links", None, GenericBrowser("links '%s'")) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 244 |         # The Lynx browser <http://lynx.browser.org/> | 
 | 245 |         if _iscommand("lynx"): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 246 |             register("lynx", None, GenericBrowser("lynx '%s'")) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 247 |         # The w3m browser <http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/> | 
 | 248 |         if _iscommand("w3m"): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 249 |             register("w3m", None, GenericBrowser("w3m '%s'")) | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 250 |  | 
| Ka-Ping Yee | 0a8c29b | 2001-03-02 02:01:40 +0000 | [diff] [blame] | 251 |     # X browsers have more in the way of options | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 252 |     if os.environ.get("DISPLAY"): | 
 | 253 |         # First, the Netscape series | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 254 |         if _iscommand("mozilla"): | 
 | 255 |             register("mozilla", None, Netscape("mozilla")) | 
 | 256 |         if _iscommand("netscape"): | 
 | 257 |             register("netscape", None, Netscape("netscape")) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 258 |  | 
 | 259 |         # Next, Mosaic -- old but still in use. | 
 | 260 |         if _iscommand("mosaic"): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 261 |             register("mosaic", None, GenericBrowser( | 
 | 262 |                 "mosaic '%s' >/dev/null &")) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 263 |  | 
 | 264 |         # Konqueror/kfm, the KDE browser. | 
| Fred Drake | fc31f26 | 2001-03-26 15:06:15 +0000 | [diff] [blame] | 265 |         if _iscommand("kfm") or _iscommand("konqueror"): | 
| Fred Drake | f4e5bd9 | 2001-04-12 22:07:27 +0000 | [diff] [blame] | 266 |             register("kfm", Konqueror, Konqueror()) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 267 |  | 
 | 268 |         # Grail, the Python browser. | 
 | 269 |         if _iscommand("grail"): | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 270 |             register("grail", Grail, None) | 
 | 271 |  | 
| Fred Drake | 3f8f164 | 2001-07-19 03:46:26 +0000 | [diff] [blame] | 272 |  | 
 | 273 | class InternetConfig: | 
 | 274 |     def open(self, url, new=0, autoraise=1): | 
 | 275 |         ic.launchurl(url) | 
 | 276 |  | 
 | 277 |     def open_new(self, url): | 
 | 278 |         self.open(url) | 
 | 279 |  | 
 | 280 |  | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 281 | # | 
 | 282 | # Platform support for Windows | 
 | 283 | # | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 284 |  | 
 | 285 | if sys.platform[:3] == "win": | 
| Guido van Rossum | cb33165 | 2001-12-03 15:51:31 +0000 | [diff] [blame] | 286 |     _tryorder = ["netscape", "windows-default"] | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 287 |     register("windows-default", WindowsDefault) | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 288 |  | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 289 | # | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 290 | # Platform support for MacOS | 
 | 291 | # | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 292 |  | 
 | 293 | try: | 
 | 294 |     import ic | 
 | 295 | except ImportError: | 
 | 296 |     pass | 
 | 297 | else: | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 298 |     # internet-config is the only supported controller on MacOS, | 
 | 299 |     # so don't mess with the default! | 
| Guido van Rossum | cb33165 | 2001-12-03 15:51:31 +0000 | [diff] [blame] | 300 |     _tryorder = ["internet-config"] | 
| Fred Drake | c70b448 | 2000-07-09 16:45:56 +0000 | [diff] [blame] | 301 |     register("internet-config", InternetConfig) | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 302 |  | 
| Martin v. Löwis | 3a89b2b | 2001-11-25 14:35:58 +0000 | [diff] [blame] | 303 | # | 
 | 304 | # Platform support for OS/2 | 
 | 305 | # | 
 | 306 |  | 
 | 307 | if sys.platform[:3] == "os2" and _iscommand("netscape.exe"): | 
| Guido van Rossum | cb33165 | 2001-12-03 15:51:31 +0000 | [diff] [blame] | 308 |     _tryorder = ["os2netscape"] | 
| Martin v. Löwis | 3a89b2b | 2001-11-25 14:35:58 +0000 | [diff] [blame] | 309 |     register("os2netscape", None, | 
 | 310 |              GenericBrowser("start netscape.exe %s")) | 
 | 311 |  | 
| Eric S. Raymond | f7f1851 | 2001-01-23 13:16:32 +0000 | [diff] [blame] | 312 | # OK, now that we know what the default preference orders for each | 
 | 313 | # platform are, allow user to override them with the BROWSER variable. | 
 | 314 | # | 
 | 315 | if os.environ.has_key("BROWSER"): | 
 | 316 |     # It's the user's responsibility to register handlers for any unknown | 
 | 317 |     # browser referenced by this value, before calling open(). | 
| Guido van Rossum | 4b402f2 | 2001-12-04 17:43:22 +0000 | [diff] [blame] | 318 |     _tryorder = os.environ["BROWSER"].split(os.pathsep) | 
| Skip Montanaro | cdab3bf | 2001-07-18 20:03:32 +0000 | [diff] [blame] | 319 |  | 
 | 320 | for cmd in _tryorder: | 
 | 321 |     if not _browsers.has_key(cmd.lower()): | 
 | 322 |         if _iscommand(cmd.lower()): | 
| Fred Drake | 925f144 | 2002-01-07 15:29:01 +0000 | [diff] [blame] | 323 |             register(cmd.lower(), None, GenericBrowser( | 
 | 324 |                 "%s '%%s'" % cmd.lower())) | 
| Jack Jansen | ff0a7b8 | 2002-03-15 13:47:32 +0000 | [diff] [blame] | 325 | cmd = None # to make del work if _tryorder was empty | 
| Neal Norwitz | f963b45 | 2002-02-11 18:11:09 +0000 | [diff] [blame] | 326 | del cmd | 
| Skip Montanaro | cdab3bf | 2001-07-18 20:03:32 +0000 | [diff] [blame] | 327 |  | 
 | 328 | _tryorder = filter(lambda x: _browsers.has_key(x.lower()) | 
 | 329 |                    or x.find("%s") > -1, _tryorder) | 
 | 330 | # what to do if _tryorder is now empty? |