]> git.ipfire.org Git - thirdparty/Python/cpython.git/blob
3244f206aa
[thirdparty/Python/cpython.git] /
1 #! /usr/bin/env python3
2 """Interfaces for launching and remotely controlling web browsers."""
3 # Maintained by Georg Brandl.
4
5 import os
6 import shlex
7 import shutil
8 import sys
9 import subprocess
10 import threading
11 import warnings
12
13 __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
14
15 class Error(Exception):
16 pass
17
18 _lock = threading.RLock()
19 _browsers = {} # Dictionary of available browser controllers
20 _tryorder = None # Preference order of available browsers
21 _os_preferred_browser = None # The preferred browser
22
23 def register(name, klass, instance=None, *, preferred=False):
24 """Register a browser connector."""
25 with _lock:
26 if _tryorder is None:
27 register_standard_browsers()
28 _browsers[name.lower()] = [klass, instance]
29
30 # Preferred browsers go to the front of the list.
31 # Need to match to the default browser returned by xdg-settings, which
32 # may be of the form e.g. "firefox.desktop".
33 if preferred or (_os_preferred_browser and name in _os_preferred_browser):
34 _tryorder.insert(0, name)
35 else:
36 _tryorder.append(name)
37
38 def get(using=None):
39 """Return a browser launcher instance appropriate for the environment."""
40 if _tryorder is None:
41 with _lock:
42 if _tryorder is None:
43 register_standard_browsers()
44 if using is not None:
45 alternatives = [using]
46 else:
47 alternatives = _tryorder
48 for browser in alternatives:
49 if '%s' in browser:
50 # User gave us a command line, split it into name and args
51 browser = shlex.split(browser)
52 if browser[-1] == '&':
53 return BackgroundBrowser(browser[:-1])
54 else:
55 return GenericBrowser(browser)
56 else:
57 # User gave us a browser name or path.
58 try:
59 command = _browsers[browser.lower()]
60 except KeyError:
61 command = _synthesize(browser)
62 if command[1] is not None:
63 return command[1]
64 elif command[0] is not None:
65 return command[0]()
66 raise Error("could not locate runnable browser")
67
68 # Please note: the following definition hides a builtin function.
69 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
70 # instead of "from webbrowser import *".
71
72 def open(url, new=0, autoraise=True):
73 """Display url using the default browser.
74
75 If possible, open url in a location determined by new.
76 - 0: the same browser window (the default).
77 - 1: a new browser window.
78 - 2: a new browser page ("tab").
79 If possible, autoraise raises the window (the default) or not.
80 """
81 if _tryorder is None:
82 with _lock:
83 if _tryorder is None:
84 register_standard_browsers()
85 for name in _tryorder:
86 browser = get(name)
87 if browser.open(url, new, autoraise):
88 return True
89 return False
90
91 def open_new(url):
92 """Open url in a new window of the default browser.
93
94 If not possible, then open url in the only browser window.
95 """
96 return open(url, 1)
97
98 def open_new_tab(url):
99 """Open url in a new page ("tab") of the default browser.
100
101 If not possible, then the behavior becomes equivalent to open_new().
102 """
103 return open(url, 2)
104
105
106 def _synthesize(browser, *, preferred=False):
107 """Attempt to synthesize a controller based on existing controllers.
108
109 This is useful to create a controller when a user specifies a path to
110 an entry in the BROWSER environment variable -- we can copy a general
111 controller to operate using a specific installation of the desired
112 browser in this way.
113
114 If we can't create a controller in this way, or if there is no
115 executable for the requested browser, return [None, None].
116
117 """
118 cmd = browser.split()[0]
119 if not shutil.which(cmd):
120 return [None, None]
121 name = os.path.basename(cmd)
122 try:
123 command = _browsers[name.lower()]
124 except KeyError:
125 return [None, None]
126 # now attempt to clone to fit the new name:
127 controller = command[1]
128 if controller and name.lower() == controller.basename:
129 import copy
130 controller = copy.copy(controller)
131 controller.name = browser
132 controller.basename = os.path.basename(browser)
133 register(browser, None, instance=controller, preferred=preferred)
134 return [None, controller]
135 return [None, None]
136
137
138 # General parent classes
139
140 class BaseBrowser(object):
141 """Parent class for all browsers. Do not use directly."""
142
143 args = ['%s']
144
145 def __init__(self, name=""):
146 self.name = name
147 self.basename = name
148
149 def open(self, url, new=0, autoraise=True):
150 raise NotImplementedError
151
152 def open_new(self, url):
153 return self.open(url, 1)
154
155 def open_new_tab(self, url):
156 return self.open(url, 2)
157
158
159 class GenericBrowser(BaseBrowser):
160 """Class for all browsers started with a command
161 and without remote functionality."""
162
163 def __init__(self, name):
164 if isinstance(name, str):
165 self.name = name
166 self.args = ["%s"]
167 else:
168 # name should be a list with arguments
169 self.name = name[0]
170 self.args = name[1:]
171 self.basename = os.path.basename(self.name)
172
173 def open(self, url, new=0, autoraise=True):
174 sys.audit("webbrowser.open", url)
175 cmdline = [self.name] + [arg.replace("%s", url)
176 for arg in self.args]
177 try:
178 if sys.platform[:3] == 'win':
179 p = subprocess.Popen(cmdline)
180 else:
181 p = subprocess.Popen(cmdline, close_fds=True)
182 return not p.wait()
183 except OSError:
184 return False
185
186
187 class BackgroundBrowser(GenericBrowser):
188 """Class for all browsers which are to be started in the
189 background."""
190
191 def open(self, url, new=0, autoraise=True):
192 cmdline = [self.name] + [arg.replace("%s", url)
193 for arg in self.args]
194 sys.audit("webbrowser.open", url)
195 try:
196 if sys.platform[:3] == 'win':
197 p = subprocess.Popen(cmdline)
198 else:
199 p = subprocess.Popen(cmdline, close_fds=True,
200 start_new_session=True)
201 return (p.poll() is None)
202 except OSError:
203 return False
204
205
206 class UnixBrowser(BaseBrowser):
207 """Parent class for all Unix browsers with remote functionality."""
208
209 raise_opts = None
210 background = False
211 redirect_stdout = True
212 # In remote_args, %s will be replaced with the requested URL. %action will
213 # be replaced depending on the value of 'new' passed to open.
214 # remote_action is used for new=0 (open). If newwin is not None, it is
215 # used for new=1 (open_new). If newtab is not None, it is used for
216 # new=3 (open_new_tab). After both substitutions are made, any empty
217 # strings in the transformed remote_args list will be removed.
218 remote_args = ['%action', '%s']
219 remote_action = None
220 remote_action_newwin = None
221 remote_action_newtab = None
222
223 def _invoke(self, args, remote, autoraise, url=None):
224 raise_opt = []
225 if remote and self.raise_opts:
226 # use autoraise argument only for remote invocation
227 autoraise = int(autoraise)
228 opt = self.raise_opts[autoraise]
229 if opt: raise_opt = [opt]
230
231 cmdline = [self.name] + raise_opt + args
232
233 if remote or self.background:
234 inout = subprocess.DEVNULL
235 else:
236 # for TTY browsers, we need stdin/out
237 inout = None
238 p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
239 stdout=(self.redirect_stdout and inout or None),
240 stderr=inout, start_new_session=True)
241 if remote:
242 # wait at most five seconds. If the subprocess is not finished, the
243 # remote invocation has (hopefully) started a new instance.
244 try:
245 rc = p.wait(5)
246 # if remote call failed, open() will try direct invocation
247 return not rc
248 except subprocess.TimeoutExpired:
249 return True
250 elif self.background:
251 if p.poll() is None:
252 return True
253 else:
254 return False
255 else:
256 return not p.wait()
257
258 def open(self, url, new=0, autoraise=True):
259 sys.audit("webbrowser.open", url)
260 if new == 0:
261 action = self.remote_action
262 elif new == 1:
263 action = self.remote_action_newwin
264 elif new == 2:
265 if self.remote_action_newtab is None:
266 action = self.remote_action_newwin
267 else:
268 action = self.remote_action_newtab
269 else:
270 raise Error("Bad 'new' parameter to open(); " +
271 "expected 0, 1, or 2, got %s" % new)
272
273 args = [arg.replace("%s", url).replace("%action", action)
274 for arg in self.remote_args]
275 args = [arg for arg in args if arg]
276 success = self._invoke(args, True, autoraise, url)
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
283
284
285 class Mozilla(UnixBrowser):
286 """Launcher class for Mozilla browsers."""
287
288 remote_args = ['%action', '%s']
289 remote_action = ""
290 remote_action_newwin = "-new-window"
291 remote_action_newtab = "-new-tab"
292 background = True
293
294
295 class Netscape(UnixBrowser):
296 """Launcher class for Netscape browser."""
297
298 raise_opts = ["-noraise", "-raise"]
299 remote_args = ['-remote', 'openURL(%s%action)']
300 remote_action = ""
301 remote_action_newwin = ",new-window"
302 remote_action_newtab = ",new-tab"
303 background = True
304
305
306 class Galeon(UnixBrowser):
307 """Launcher class for Galeon/Epiphany browsers."""
308
309 raise_opts = ["-noraise", ""]
310 remote_args = ['%action', '%s']
311 remote_action = "-n"
312 remote_action_newwin = "-w"
313 background = True
314
315
316 class Chrome(UnixBrowser):
317 "Launcher class for Google Chrome browser."
318
319 remote_args = ['%action', '%s']
320 remote_action = ""
321 remote_action_newwin = "--new-window"
322 remote_action_newtab = ""
323 background = True
324
325 Chromium = Chrome
326
327
328 class Opera(UnixBrowser):
329 "Launcher class for Opera browser."
330
331 remote_args = ['%action', '%s']
332 remote_action = ""
333 remote_action_newwin = "--new-window"
334 remote_action_newtab = ""
335 background = True
336
337
338 class Elinks(UnixBrowser):
339 "Launcher class for Elinks browsers."
340
341 remote_args = ['-remote', 'openURL(%s%action)']
342 remote_action = ""
343 remote_action_newwin = ",new-window"
344 remote_action_newtab = ",new-tab"
345 background = False
346
347 # elinks doesn't like its stdout to be redirected -
348 # it uses redirected stdout as a signal to do -dump
349 redirect_stdout = False
350
351
352 class Konqueror(BaseBrowser):
353 """Controller for the KDE File Manager (kfm, or Konqueror).
354
355 See the output of ``kfmclient --commands``
356 for more information on the Konqueror remote-control interface.
357 """
358
359 def open(self, url, new=0, autoraise=True):
360 sys.audit("webbrowser.open", url)
361 # XXX Currently I know no way to prevent KFM from opening a new win.
362 if new == 2:
363 action = "newTab"
364 else:
365 action = "openURL"
366
367 devnull = subprocess.DEVNULL
368
369 try:
370 p = subprocess.Popen(["kfmclient", action, url],
371 close_fds=True, stdin=devnull,
372 stdout=devnull, stderr=devnull)
373 except OSError:
374 # fall through to next variant
375 pass
376 else:
377 p.wait()
378 # kfmclient's return code unfortunately has no meaning as it seems
379 return True
380
381 try:
382 p = subprocess.Popen(["konqueror", "--silent", url],
383 close_fds=True, stdin=devnull,
384 stdout=devnull, stderr=devnull,
385 start_new_session=True)
386 except OSError:
387 # fall through to next variant
388 pass
389 else:
390 if p.poll() is None:
391 # Should be running now.
392 return True
393
394 try:
395 p = subprocess.Popen(["kfm", "-d", url],
396 close_fds=True, stdin=devnull,
397 stdout=devnull, stderr=devnull,
398 start_new_session=True)
399 except OSError:
400 return False
401 else:
402 return (p.poll() is None)
403
404
405 class Grail(BaseBrowser):
406 # There should be a way to maintain a connection to Grail, but the
407 # Grail remote control protocol doesn't really allow that at this
408 # point. It probably never will!
409 def _find_grail_rc(self):
410 import glob
411 import pwd
412 import socket
413 import tempfile
414 tempdir = os.path.join(tempfile.gettempdir(),
415 ".grail-unix")
416 user = pwd.getpwuid(os.getuid())[0]
417 filename = os.path.join(glob.escape(tempdir), glob.escape(user) + "-*")
418 maybes = glob.glob(filename)
419 if not maybes:
420 return None
421 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
422 for fn in maybes:
423 # need to PING each one until we find one that's live
424 try:
425 s.connect(fn)
426 except OSError:
427 # no good; attempt to clean it out, but don't fail:
428 try:
429 os.unlink(fn)
430 except OSError:
431 pass
432 else:
433 return s
434
435 def _remote(self, action):
436 s = self._find_grail_rc()
437 if not s:
438 return 0
439 s.send(action)
440 s.close()
441 return 1
442
443 def open(self, url, new=0, autoraise=True):
444 sys.audit("webbrowser.open", url)
445 if new:
446 ok = self._remote("LOADNEW " + url)
447 else:
448 ok = self._remote("LOAD " + url)
449 return ok
450
451
452 #
453 # Platform support for Unix
454 #
455
456 # These are the right tests because all these Unix browsers require either
457 # a console terminal or an X display to run.
458
459 def register_X_browsers():
460
461 # use xdg-open if around
462 if shutil.which("xdg-open"):
463 register("xdg-open", None, BackgroundBrowser("xdg-open"))
464
465 # Opens an appropriate browser for the URL scheme according to
466 # freedesktop.org settings (GNOME, KDE, XFCE, etc.)
467 if shutil.which("gio"):
468 register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"]))
469
470 # The default KDE browser
471 if "KDE_FULL_SESSION" in os.environ and shutil.which("kfmclient"):
472 register("kfmclient", Konqueror, Konqueror("kfmclient"))
473
474 if shutil.which("x-www-browser"):
475 register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
476
477 # The Mozilla browsers
478 for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
479 if shutil.which(browser):
480 register(browser, None, Mozilla(browser))
481
482 # The Netscape and old Mozilla browsers
483 for browser in ("mozilla-firefox",
484 "mozilla-firebird", "firebird",
485 "mozilla", "netscape"):
486 if shutil.which(browser):
487 register(browser, None, Netscape(browser))
488
489 # Konqueror/kfm, the KDE browser.
490 if shutil.which("kfm"):
491 register("kfm", Konqueror, Konqueror("kfm"))
492 elif shutil.which("konqueror"):
493 register("konqueror", Konqueror, Konqueror("konqueror"))
494
495 # Gnome's Galeon and Epiphany
496 for browser in ("galeon", "epiphany"):
497 if shutil.which(browser):
498 register(browser, None, Galeon(browser))
499
500 # Skipstone, another Gtk/Mozilla based browser
501 if shutil.which("skipstone"):
502 register("skipstone", None, BackgroundBrowser("skipstone"))
503
504 # Google Chrome/Chromium browsers
505 for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):
506 if shutil.which(browser):
507 register(browser, None, Chrome(browser))
508
509 # Opera, quite popular
510 if shutil.which("opera"):
511 register("opera", None, Opera("opera"))
512
513 # Next, Mosaic -- old but still in use.
514 if shutil.which("mosaic"):
515 register("mosaic", None, BackgroundBrowser("mosaic"))
516
517 # Grail, the Python browser. Does anybody still use it?
518 if shutil.which("grail"):
519 register("grail", Grail, None)
520
521 def register_standard_browsers():
522 global _tryorder
523 _tryorder = []
524
525 if sys.platform == 'darwin':
526 register("MacOSX", None, MacOSXOSAScript('default'))
527 register("chrome", None, MacOSXOSAScript('chrome'))
528 register("firefox", None, MacOSXOSAScript('firefox'))
529 register("safari", None, MacOSXOSAScript('safari'))
530 # OS X can use below Unix support (but we prefer using the OS X
531 # specific stuff)
532
533 if sys.platform == "serenityos":
534 # SerenityOS webbrowser, simply called "Browser".
535 register("Browser", None, BackgroundBrowser("Browser"))
536
537 if sys.platform[:3] == "win":
538 # First try to use the default Windows browser
539 register("windows-default", WindowsDefault)
540
541 # Detect some common Windows browsers, fallback to IE
542 iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
543 "Internet Explorer\\IEXPLORE.EXE")
544 for browser in ("firefox", "firebird", "seamonkey", "mozilla",
545 "netscape", "opera", iexplore):
546 if shutil.which(browser):
547 register(browser, None, BackgroundBrowser(browser))
548 else:
549 # Prefer X browsers if present
550 if os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"):
551 try:
552 cmd = "xdg-settings get default-web-browser".split()
553 raw_result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
554 result = raw_result.decode().strip()
555 except (FileNotFoundError, subprocess.CalledProcessError, PermissionError, NotADirectoryError) :
556 pass
557 else:
558 global _os_preferred_browser
559 _os_preferred_browser = result
560
561 register_X_browsers()
562
563 # Also try console browsers
564 if os.environ.get("TERM"):
565 if shutil.which("www-browser"):
566 register("www-browser", None, GenericBrowser("www-browser"))
567 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
568 if shutil.which("links"):
569 register("links", None, GenericBrowser("links"))
570 if shutil.which("elinks"):
571 register("elinks", None, Elinks("elinks"))
572 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
573 if shutil.which("lynx"):
574 register("lynx", None, GenericBrowser("lynx"))
575 # The w3m browser <http://w3m.sourceforge.net/>
576 if shutil.which("w3m"):
577 register("w3m", None, GenericBrowser("w3m"))
578
579 # OK, now that we know what the default preference orders for each
580 # platform are, allow user to override them with the BROWSER variable.
581 if "BROWSER" in os.environ:
582 userchoices = os.environ["BROWSER"].split(os.pathsep)
583 userchoices.reverse()
584
585 # Treat choices in same way as if passed into get() but do register
586 # and prepend to _tryorder
587 for cmdline in userchoices:
588 if cmdline != '':
589 cmd = _synthesize(cmdline, preferred=True)
590 if cmd[1] is None:
591 register(cmdline, None, GenericBrowser(cmdline), preferred=True)
592
593 # what to do if _tryorder is now empty?
594
595
596 #
597 # Platform support for Windows
598 #
599
600 if sys.platform[:3] == "win":
601 class WindowsDefault(BaseBrowser):
602 def open(self, url, new=0, autoraise=True):
603 sys.audit("webbrowser.open", url)
604 try:
605 os.startfile(url)
606 except OSError:
607 # [Error 22] No application is associated with the specified
608 # file for this operation: '<URL>'
609 return False
610 else:
611 return True
612
613 #
614 # Platform support for MacOS
615 #
616
617 if sys.platform == 'darwin':
618 # Adapted from patch submitted to SourceForge by Steven J. Burr
619 class MacOSX(BaseBrowser):
620 """Launcher class for Aqua browsers on Mac OS X
621
622 Optionally specify a browser name on instantiation. Note that this
623 will not work for Aqua browsers if the user has moved the application
624 package after installation.
625
626 If no browser is specified, the default browser, as specified in the
627 Internet System Preferences panel, will be used.
628 """
629 def __init__(self, name):
630 warnings.warn(f'{self.__class__.__name__} is deprecated in 3.11'
631 ' use MacOSXOSAScript instead.', DeprecationWarning, stacklevel=2)
632 self.name = name
633
634 def open(self, url, new=0, autoraise=True):
635 sys.audit("webbrowser.open", url)
636 assert "'" not in url
637 # hack for local urls
638 if not ':' in url:
639 url = 'file:'+url
640
641 # new must be 0 or 1
642 new = int(bool(new))
643 if self.name == "default":
644 # User called open, open_new or get without a browser parameter
645 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
646 else:
647 # User called get and chose a browser
648 if self.name == "OmniWeb":
649 toWindow = ""
650 else:
651 # Include toWindow parameter of OpenURL command for browsers
652 # that support it. 0 == new window; -1 == existing
653 toWindow = "toWindow %d" % (new - 1)
654 cmd = 'OpenURL "%s"' % url.replace('"', '%22')
655 script = '''tell application "%s"
656 activate
657 %s %s
658 end tell''' % (self.name, cmd, toWindow)
659 # Open pipe to AppleScript through osascript command
660 osapipe = os.popen("osascript", "w")
661 if osapipe is None:
662 return False
663 # Write script to osascript's stdin
664 osapipe.write(script)
665 rc = osapipe.close()
666 return not rc
667
668 class MacOSXOSAScript(BaseBrowser):
669 def __init__(self, name):
670 self._name = name
671
672 def open(self, url, new=0, autoraise=True):
673 if self._name == 'default':
674 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
675 else:
676 script = '''
677 tell application "%s"
678 activate
679 open location "%s"
680 end
681 '''%(self._name, url.replace('"', '%22'))
682
683 osapipe = os.popen("osascript", "w")
684 if osapipe is None:
685 return False
686
687 osapipe.write(script)
688 rc = osapipe.close()
689 return not rc
690
691
692 def main():
693 import getopt
694 usage = """Usage: %s [-n | -t] url
695 -n: open new window
696 -t: open new tab""" % sys.argv[0]
697 try:
698 opts, args = getopt.getopt(sys.argv[1:], 'ntd')
699 except getopt.error as msg:
700 print(msg, file=sys.stderr)
701 print(usage, file=sys.stderr)
702 sys.exit(1)
703 new_win = 0
704 for o, a in opts:
705 if o == '-n': new_win = 1
706 elif o == '-t': new_win = 2
707 if len(args) != 1:
708 print(usage, file=sys.stderr)
709 sys.exit(1)
710
711 url = args[0]
712 open(url, new_win)
713
714 print("\a")
715
716 if __name__ == "__main__":
717 main()