#!/usr/bin/python3 # 1. Standard library imports. import configparser import gettext from io import BytesIO import locale import os from random import choice import shutil import string import sys import tempfile import urllib.error import urllib.parse import urllib.request import threading import traceback from typing import Optional # 2. Related third party imports. from gi.repository import GObject import PIL.Image import requests # Note: BeautifulSoup is an optional import supporting another way of getting a website's favicons. # Used as a decorator to run things in the background def _async(func): def wrapper(*args, **kwargs): thread = threading.Thread(target=func, args=args, kwargs=kwargs) thread.daemon = True thread.start() return thread return wrapper # Used as a decorator to run things in the main loop, from another thread def idle(func): def wrapper(*args): GObject.idle_add(func, *args) return wrapper # i18n APP = 'webapp-manager' LOCALE_DIR = "/usr/share/locale" locale.bindtextdomain(APP, LOCALE_DIR) gettext.bindtextdomain(APP, LOCALE_DIR) gettext.textdomain(APP) _ = gettext.gettext # Constants ICE_DIR = os.path.expanduser("~/.local/share/ice") APPS_DIR = os.path.expanduser("~/.local/share/applications") PROFILES_DIR = os.path.join(ICE_DIR, "profiles") FIREFOX_PROFILES_DIR = os.path.join(ICE_DIR, "firefox") FIREFOX_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/org.mozilla.firefox/data/ice/firefox") FIREFOX_SNAP_PROFILES_DIR = os.path.expanduser("~/snap/firefox/common/.mozilla/firefox") LIBREWOLF_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/io.gitlab.librewolf-community/data/ice/librewolf") WATERFOX_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/net.waterfox.waterfox/data") FLOORP_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/one.ablaze.floorp/data") EPIPHANY_PROFILES_DIR = os.path.join(ICE_DIR, "epiphany") FALKON_PROFILES_DIR = os.path.join(ICE_DIR, "falkon") ZEN_FLATPAK_PROFILES_DIR = os.path.expanduser("~/.var/app/app.zen_browser.zen/data/ice/zen/") ICONS_DIR = os.path.join(ICE_DIR, "icons") BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP, BROWSER_TYPE_LIBREWOLF_FLATPAK, BROWSER_TYPE_WATERFOX_FLATPAK, BROWSER_TYPE_FLOORP_FLATPAK, BROWSER_TYPE_CHROMIUM, BROWSER_TYPE_EPIPHANY, BROWSER_TYPE_FALKON, BROWSER_TYPE_ZEN_FLATPAK = range(10) class Browser: def __init__(self, browser_type, name, exec_path, test_path): self.browser_type = browser_type self.name = name self.exec_path = exec_path self.test_path = test_path # This is a data structure representing # the app menu item (path, name, icon..etc.) class WebAppLauncher: def __init__(self, path, codename): self.path = path self.codename = codename self.web_browser = None self.name = None self.desc = None self.icon = None self.is_valid = False self.exec = None self.category = None self.url = "" self.custom_parameters = "" self.isolate_profile = False self.navbar = False self.privatewindow = False is_webapp = False with open(path) as desktop_file: for line in desktop_file: line = line.strip() # Identify if the app is a webapp if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line: is_webapp = True continue if "Name=" in line: self.name = line.replace("Name=", "") continue if "Comment=" in line: self.desc = line.replace("Comment=", "") if self.desc == _("Web App"): self.desc = "" continue if "Icon=" in line: self.icon = line.replace("Icon=", "") continue if "Exec=" in line: self.exec = line.replace("Exec=", "") continue if "Categories=" in line: self.category = line.replace("Categories=", "").replace("GTK;", "").replace(";", "") continue if "X-WebApp-Browser=" in line: self.web_browser = line.replace("X-WebApp-Browser=", "") continue if "X-WebApp-URL=" in line: self.url = line.replace("X-WebApp-URL=", "") continue if "X-WebApp-CustomParameters" in line: self.custom_parameters = line.replace("X-WebApp-CustomParameters=", "") continue if "X-WebApp-Isolated" in line: self.isolate_profile = line.replace("X-WebApp-Isolated=", "").lower() == "true" continue if "X-WebApp-Navbar" in line: self.navbar = line.replace("X-WebApp-Navbar=", "").lower() == "true" continue if "X-WebApp-PrivateWindow" in line: self.privatewindow = line.replace("X-WebApp-PrivateWindow=", "").lower() == "true" continue if is_webapp and self.name is not None and self.icon is not None: self.is_valid = True # This is the backend. # It contains utility functions to load, # save and delete webapps. class WebAppManager: def __init__(self): for directory in [ICE_DIR, APPS_DIR, PROFILES_DIR, FIREFOX_PROFILES_DIR, FIREFOX_FLATPAK_PROFILES_DIR, ICONS_DIR, EPIPHANY_PROFILES_DIR, FALKON_PROFILES_DIR]: if not os.path.exists(directory): os.makedirs(directory) def get_webapps(self): webapps = [] for filename in os.listdir(APPS_DIR): if filename.lower().startswith("webapp-") and filename.endswith(".desktop"): path = os.path.join(APPS_DIR, filename) codename = filename.replace("webapp-", "").replace("WebApp-", "").replace(".desktop", "") if not os.path.isdir(path): try: webapp = WebAppLauncher(path, codename) if webapp.is_valid: webapps.append(webapp) except Exception: print("Could not create webapp for path", path) traceback.print_exc() return webapps @staticmethod def get_supported_browsers(): # type, name, exec, test return [Browser(BROWSER_TYPE_FIREFOX, "Firefox", "firefox", "/usr/bin/firefox"), Browser(BROWSER_TYPE_FIREFOX, "Firefox Developer Edition", "firefox-developer-edition", "/usr/bin/firefox-developer-edition"), Browser(BROWSER_TYPE_FIREFOX, "Firefox Nightly", "firefox-nightly", "/usr/bin/firefox-nightly"), Browser(BROWSER_TYPE_FIREFOX, "Firefox Extended Support Release", "firefox-esr", "/usr/bin/firefox-esr"), Browser(BROWSER_TYPE_FIREFOX_FLATPAK, "Firefox (Flatpak)", "/var/lib/flatpak/exports/bin/org.mozilla.firefox", "/var/lib/flatpak/exports/bin/org.mozilla.firefox"), Browser(BROWSER_TYPE_FIREFOX_FLATPAK, "Firefox (Flatpak)", ".local/share/flatpak/exports/bin/org.mozilla.firefox", ".local/share/flatpak/exports/bin/org.mozilla.firefox"), Browser(BROWSER_TYPE_ZEN_FLATPAK, "Zen (Flatpak)", "/var/lib/flatpak/exports/bin/app.zen_browser.zen", "/var/lib/flatpak/exports/bin/app.zen_browser.zen"), Browser(BROWSER_TYPE_ZEN_FLATPAK, "Zen (Flatpak)", ".local/share/flatpak/exports/bin/app.zen_browser.zen", ".local/share/flatpak/exports/bin/app.zen_browser.zen"), Browser(BROWSER_TYPE_FIREFOX_SNAP, "Firefox (Snap)", "/snap/bin/firefox", "/snap/bin/firefox"), Browser(BROWSER_TYPE_CHROMIUM, "Brave", "brave", "/usr/bin/brave"), Browser(BROWSER_TYPE_CHROMIUM, "Brave Browser", "brave-browser", "/usr/bin/brave-browser"), Browser(BROWSER_TYPE_CHROMIUM, "Brave (Bin)", "brave-bin", "/usr/bin/brave-bin"), Browser(BROWSER_TYPE_CHROMIUM, "Chrome", "google-chrome-stable", "/usr/bin/google-chrome-stable"), Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Beta)", "google-chrome-beta", "/usr/bin/google-chrome-beta"), Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Flatpak)", "/var/lib/flatpak/exports/bin/com.google.Chrome", "/var/lib/flatpak/exports/bin/com.google.Chrome"), Browser(BROWSER_TYPE_CHROMIUM, "Chrome (Flatpak)", ".local/share/flatpak/exports/bin/com.google.Chrome", ".local/share/flatpak/exports/bin/com.google.Chrome"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium", "chromium", "/usr/bin/chromium"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium (chromium-browser)", "chromium-browser", "/usr/bin/chromium-browser"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Snap)", "chromium", "/snap/bin/chromium"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Bin)", "chromium-bin", "/usr/bin/chromium-bin-browser"), Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium", "ungoogled-chromium", "/usr/bin/ungoogled-chromium"), Browser(BROWSER_TYPE_EPIPHANY, "Epiphany", "epiphany", "/usr/bin/epiphany"), Browser(BROWSER_TYPE_FIREFOX, "LibreWolf", "librewolf", "/usr/bin/librewolf"), Browser(BROWSER_TYPE_LIBREWOLF_FLATPAK, "LibreWolf (Flatpak)", "/var/lib/flatpak/exports/bin/io.gitlab.librewolf-community", "/var/lib/flatpak/exports/bin/io.gitlab.librewolf-community"), Browser(BROWSER_TYPE_LIBREWOLF_FLATPAK, "LibreWolf (Flatpak)", ".local/share/flatpak/exports/bin/io.gitlab.librewolf-community", ".local/share/flatpak/exports/bin/io.gitlab.librewolf-community"), Browser(BROWSER_TYPE_FIREFOX, "Waterfox", "waterfox", "/usr/bin/waterfox"), Browser(BROWSER_TYPE_FIREFOX, "Waterfox Current", "waterfox-current", "/usr/bin/waterfox-current"), Browser(BROWSER_TYPE_FIREFOX, "Waterfox Classic", "waterfox-classic", "/usr/bin/waterfox-classic"), Browser(BROWSER_TYPE_FIREFOX, "Waterfox 3rd Generation", "waterfox-g3", "/usr/bin/waterfox-g3"), Browser(BROWSER_TYPE_FIREFOX, "Waterfox 4th Generation", "waterfox-g4", "/usr/bin/waterfox-g4"), Browser(BROWSER_TYPE_FIREFOX, "Floorp", "floorp", "/usr/bin/floorp"), Browser(BROWSER_TYPE_WATERFOX_FLATPAK, "Waterfox (Flatpak)", "/var/lib/flatpak/exports/bin/net.waterfox.waterfox", "/var/lib/flatpak/exports/bin/net.waterfox.waterfox"), Browser(BROWSER_TYPE_WATERFOX_FLATPAK, "Waterfox (Flatpak)", ".local/share/flatpak/exports/bin/net.waterfox.waterfox", ".local/share/flatpak/exports/bin/net.waterfox.waterfox"), Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi", "vivaldi-stable", "/usr/bin/vivaldi-stable"), Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi Snapshot", "vivaldi-snapshot", "/usr/bin/vivaldi-snapshot"), Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi (Flatpak)", "/var/lib/flatpak/exports/bin/com.vivaldi.Vivaldi", "/var/lib/flatpak/exports/bin/com.vivaldi.Vivaldi"), Browser(BROWSER_TYPE_CHROMIUM, "Vivaldi (Flatpak)", ".local/share/flatpak/exports/bin/com.vivaldi.Vivaldi", ".local/share/flatpak/exports/bin/com.vivaldi.Vivaldi"), Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge", "microsoft-edge-stable", "/usr/bin/microsoft-edge-stable"), Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge Beta", "microsoft-edge-beta", "/usr/bin/microsoft-edge-beta"), Browser(BROWSER_TYPE_CHROMIUM, "Microsoft Edge Dev", "microsoft-edge-dev", "/usr/bin/microsoft-edge-dev"), Browser(BROWSER_TYPE_CHROMIUM, "FlashPeak Slimjet", "flashpeak-slimjet", "/usr/bin/flashpeak-slimjet"), Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium (Flatpak)", "/var/lib/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium", "/var/lib/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium"), Browser(BROWSER_TYPE_CHROMIUM, "Ungoogled Chromium (Flatpak)", ".local/share/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium", ".local/share/flatpak/exports/bin/io.github.ungoogled_software.ungoogled_chromium"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Flatpak)", "/var/lib/flatpak/exports/bin/org.chromium.Chromium", "/var/lib/flatpak/exports/bin/org.chromium.Chromium"), Browser(BROWSER_TYPE_CHROMIUM, "Chromium (Flatpak)", ".local/share/flatpak/exports/bin/org.chromium.Chromium", ".local/share/flatpak/exports/bin/org.chromium.Chromium"), Browser(BROWSER_TYPE_FALKON, "Falkon", "falkon", "/usr/bin/falkon"), Browser(BROWSER_TYPE_CHROMIUM, "Edge (Flatpak)", "/var/lib/flatpak/exports/bin/com.microsoft.Edge", "/var/lib/flatpak/exports/bin/com.microsoft.Edge"), Browser(BROWSER_TYPE_CHROMIUM, "Edge (Flatpak)", ".local/share/flatpak/exports/bin/com.microsoft.Edge", ".local/share/flatpak/exports/bin/com.microsoft.Edge"), Browser(BROWSER_TYPE_CHROMIUM, "Brave (Flatpak)", "/var/lib/flatpak/exports/bin/com.brave.Browser", "/var/lib/flatpak/exports/bin/com.brave.Browser"), Browser(BROWSER_TYPE_CHROMIUM, "Brave (Flatpak)", ".local/share/flatpak/exports/bin/com.brave.Browser", ".local/share/flatpak/exports/bin/com.brave.Browser"), Browser(BROWSER_TYPE_CHROMIUM, "Yandex", "yandex-browser", "/usr/bin/yandex-browser"), Browser(BROWSER_TYPE_FALKON, "Falkon (Flatpak)", "/var/lib/flatpak/exports/bin/org.kde.falkon", "/var/lib/flatpak/exports/bin/org.kde.falkon"), Browser(BROWSER_TYPE_FALKON, "Falkon (Flatpak)", ".local/share/flatpak/exports/bin/org.kde.falkon", ".local/share/flatpak/exports/bin/org.kde.falkon"), Browser(BROWSER_TYPE_CHROMIUM, "Naver Whale", "naver-whale-stable", "/usr/bin/naver-whale-stable"), Browser(BROWSER_TYPE_CHROMIUM, "Yandex (Flatpak)", "/var/lib/flatpak/exports/bin/ru.yandex.Browser", "/var/lib/flatpak/exports/bin/ru.yandex.Browser"), Browser(BROWSER_TYPE_CHROMIUM, "Yandex (Flatpak)", ".local/share/flatpak/exports/bin/ru.yandex.Browser", ".local/share/flatpak/exports/bin/ru.yandex.Browser"), Browser(BROWSER_TYPE_CHROMIUM, "Thorium", "thorium-browser", "/usr/bin/thorium-browser"), Browser(BROWSER_TYPE_FIREFOX, "Floorp", "floorp", "/usr/bin/floorp"), Browser(BROWSER_TYPE_FLOORP_FLATPAK, "Floorp (Flatpak)", "/var/lib/flatpak/exports/bin/one.ablaze.floorp", "/var/lib/flatpak/exports/bin/one.ablaze.floorp"), Browser(BROWSER_TYPE_FLOORP_FLATPAK, "Floorp (Flatpak)", ".local/share/flatpak/exports/bin/one.ablaze.floorp", ".local/share/flatpak/exports/bin/one.ablaze.floorp") ] def delete_webbapp(self, webapp): shutil.rmtree(os.path.join(FIREFOX_PROFILES_DIR, webapp.codename), ignore_errors=True) shutil.rmtree(os.path.join(FIREFOX_FLATPAK_PROFILES_DIR, webapp.codename), ignore_errors=True) shutil.rmtree(os.path.join(ZEN_FLATPAK_PROFILES_DIR, webapp.codename), ignore_errors=True) shutil.rmtree(os.path.join(FIREFOX_SNAP_PROFILES_DIR, webapp.codename), ignore_errors=True) shutil.rmtree(os.path.join(PROFILES_DIR, webapp.codename), ignore_errors=True) # first remove symlinks then others if os.path.exists(webapp.path): os.remove(webapp.path) epiphany_orig_prof_dir=os.path.join(os.path.expanduser("~/.local/share"), "org.gnome.Epiphany.WebApp-" + webapp.codename) if os.path.exists(epiphany_orig_prof_dir): os.remove(epiphany_orig_prof_dir) shutil.rmtree(os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-%s" % webapp.codename), ignore_errors=True) falkon_orig_prof_dir = os.path.join(os.path.expanduser("~/.config/falkon/profiles"), webapp.codename) if os.path.exists(falkon_orig_prof_dir): os.remove(falkon_orig_prof_dir) shutil.rmtree(os.path.join(FALKON_PROFILES_DIR, webapp.codename), ignore_errors=True) def create_webapp(self, name, desc, url, icon, category, browser, custom_parameters, isolate_profile=True, navbar=False, privatewindow=False): # Generate a 4 digit random code (to prevent name collisions, so we can define multiple launchers with the same name) random_code = ''.join(choice(string.digits) for _ in range(4)) codename = "".join(filter(str.isalpha, name)) + random_code path = os.path.join(APPS_DIR, "WebApp-%s.desktop" % codename) if not desc: desc = _("Web App") with open(path, 'w') as desktop_file: desktop_file.write("[Desktop Entry]\n") desktop_file.write("Version=1.0\n") desktop_file.write("Name=%s\n" % name) desktop_file.write("Comment=%s\n" % desc) exec_string = self.get_exec_string(browser, codename, custom_parameters, icon, isolate_profile, navbar, privatewindow, url) desktop_file.write("Exec=%s\n" % exec_string) desktop_file.write("Terminal=false\n") desktop_file.write("X-MultipleArgs=false\n") desktop_file.write("Type=Application\n") desktop_file.write("Icon=%s\n" % icon) desktop_file.write("Categories=GTK;%s;\n" % category) desktop_file.write("MimeType=text/html;text/xml;application/xhtml_xml;\n") desktop_file.write("StartupWMClass=WebApp-%s\n" % codename) desktop_file.write("StartupNotify=true\n") desktop_file.write("X-WebApp-Browser=%s\n" % browser.name) desktop_file.write("X-WebApp-URL=%s\n" % url) desktop_file.write("X-WebApp-CustomParameters=%s\n" % custom_parameters) desktop_file.write("X-WebApp-Navbar=%s\n" % bool_to_string(navbar)) desktop_file.write("X-WebApp-PrivateWindow=%s\n" % bool_to_string(privatewindow)) desktop_file.write("X-WebApp-Isolated=%s\n" % bool_to_string(isolate_profile)) if browser.browser_type == BROWSER_TYPE_EPIPHANY: # Move the desktop file and create a symlink epiphany_profile_path = os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-" + codename) new_path = os.path.join(epiphany_profile_path, "org.gnome.Epiphany.WebApp-%s.desktop" % codename) os.makedirs(epiphany_profile_path) os.replace(path, new_path) os.symlink(new_path, path) # copy the icon to profile directory new_icon=os.path.join(epiphany_profile_path, "app-icon.png") shutil.copy(icon, new_icon) # required for app mode. create an empty file .app app_mode_file=os.path.join(epiphany_profile_path, ".app") with open(app_mode_file, 'w') as fp: pass if browser.browser_type == BROWSER_TYPE_FALKON: falkon_profile_path = os.path.join(FALKON_PROFILES_DIR, codename) os.makedirs(falkon_profile_path) # Create symlink of profile dir at ~/.config/falkon/profiles falkon_orig_prof_dir = os.path.join(os.path.expanduser("~/.config/falkon/profiles"), codename) os.symlink(falkon_profile_path, falkon_orig_prof_dir) def get_exec_string(self, browser, codename, custom_parameters, icon, isolate_profile, navbar, privatewindow, url): if browser.browser_type in [BROWSER_TYPE_FIREFOX, BROWSER_TYPE_FIREFOX_FLATPAK, BROWSER_TYPE_FIREFOX_SNAP, BROWSER_TYPE_ZEN_FLATPAK]: # Firefox based if browser.browser_type == BROWSER_TYPE_FIREFOX: firefox_profiles_dir = FIREFOX_PROFILES_DIR elif browser.browser_type == BROWSER_TYPE_FIREFOX_FLATPAK: firefox_profiles_dir = FIREFOX_FLATPAK_PROFILES_DIR elif browser.browser_type == BROWSER_TYPE_ZEN_FLATPAK: firefox_profiles_dir = ZEN_FLATPAK_PROFILES_DIR else: firefox_profiles_dir = FIREFOX_SNAP_PROFILES_DIR firefox_profile_path = os.path.join(firefox_profiles_dir, codename) exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + " --class WebApp-" + codename + " --name WebApp-" + codename + " --profile " + firefox_profile_path + " --no-remote") if privatewindow: exec_string += " --private-window" if custom_parameters: exec_string += " {}".format(custom_parameters) exec_string += " \"" + url + "\"" + "'" # Create a Firefox profile shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) if navbar: shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', os.path.join(firefox_profile_path, "chrome", "userChrome.css")) elif browser.browser_type == BROWSER_TYPE_LIBREWOLF_FLATPAK: # LibreWolf flatpak firefox_profiles_dir = LIBREWOLF_FLATPAK_PROFILES_DIR firefox_profile_path = os.path.join(firefox_profiles_dir, codename) exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + " --class WebApp-" + codename + " --name WebApp-" + codename + " --profile " + firefox_profile_path + " --no-remote") if privatewindow: exec_string += " --private-window" if custom_parameters: exec_string += " {}".format(custom_parameters) exec_string += " \"" + url + "\"" + "'" # Create a Firefox profile shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) if navbar: shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', os.path.join(firefox_profile_path, "chrome", "userChrome.css")) elif browser.browser_type == BROWSER_TYPE_FLOORP_FLATPAK: # Floorp flatpak firefox_profiles_dir = FLOORP_FLATPAK_PROFILES_DIR firefox_profile_path = os.path.join(firefox_profiles_dir, codename) exec_string = ("sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"" + icon + "\" " + browser.exec_path + " --class WebApp-" + codename + " --name WebApp-" + codename + " --profile " + firefox_profile_path + " --no-remote") if privatewindow: exec_string += " --private-window" if custom_parameters: exec_string += " {}".format(custom_parameters) exec_string += " \"" + url + "\"" + "'" # Create a Firefox profile shutil.copytree('/usr/share/webapp-manager/firefox/profile', firefox_profile_path, dirs_exist_ok = True) if navbar: shutil.copy('/usr/share/webapp-manager/firefox/userChrome-with-navbar.css', os.path.join(firefox_profile_path, "chrome", "userChrome.css")) elif browser.browser_type == BROWSER_TYPE_EPIPHANY: # Epiphany based epiphany_profile_path = os.path.join(EPIPHANY_PROFILES_DIR, "org.gnome.Epiphany.WebApp-" + codename) # Create symlink of profile dir at ~/.local/share epiphany_orig_prof_dir = os.path.join(os.path.expanduser("~/.local/share"), "org.gnome.Epiphany.WebApp-" + codename) os.symlink(epiphany_profile_path, epiphany_orig_prof_dir) exec_string = browser.exec_path exec_string += " --application-mode " exec_string += " --profile=\"" + epiphany_orig_prof_dir + "\"" exec_string += " \"" + url + "\"" if custom_parameters: exec_string += " {}".format(custom_parameters) elif browser.browser_type == BROWSER_TYPE_FALKON: # KDE Falkon exec_string = browser.exec_path exec_string += " --wmclass=WebApp-" + codename if isolate_profile: exec_string += " --profile=" + codename if privatewindow: exec_string += " --private-browsing" if custom_parameters: exec_string += " {}".format(custom_parameters) exec_string += " --no-remote " + url else: # Chromium based if isolate_profile: profile_path = os.path.join(PROFILES_DIR, codename) exec_string = (browser.exec_path + " --app=" + "\"" + url + "\"" + " --class=WebApp-" + codename + " --name=WebApp-" + codename + " --user-data-dir=" + profile_path) else: exec_string = (browser.exec_path + " --app=" + "\"" + url + "\"" + " --class=WebApp-" + codename + " --name=WebApp-" + codename) if privatewindow: if browser.name == "Microsoft Edge": exec_string += " --inprivate" elif browser.name == "Microsoft Edge Beta": exec_string += " --inprivate" elif browser.name == "Microsoft Edge Dev": exec_string += " --inprivate" else: exec_string += " --incognito" if custom_parameters: exec_string += " {}".format(custom_parameters) return exec_string def edit_webapp(self, path, name, desc, browser, url, icon, category, custom_parameters, codename, isolate_profile, navbar, privatewindow): if not desc: desc = _("Web App") config = configparser.RawConfigParser() config.optionxform = str config.read(path) config.set("Desktop Entry", "Name", name) config.set("Desktop Entry", "Icon", icon) config.set("Desktop Entry", "Comment", desc) config.set("Desktop Entry", "Categories", "GTK;%s;" % category) try: # This will raise an exception on legacy apps which # have no X-WebApp-URL and X-WebApp-Browser exec_line = self.get_exec_string(browser, codename, custom_parameters, icon, isolate_profile, navbar, privatewindow, url) config.set("Desktop Entry", "Exec", exec_line) config.set("Desktop Entry", "X-WebApp-Browser", browser.name) config.set("Desktop Entry", "X-WebApp-URL", url) config.set("Desktop Entry", "X-WebApp-CustomParameters", custom_parameters) config.set("Desktop Entry", "X-WebApp-Isolated", bool_to_string(isolate_profile)) config.set("Desktop Entry", "X-WebApp-Navbar", bool_to_string(navbar)) config.set("Desktop Entry", "X-WebApp-PrivateWindow", bool_to_string(privatewindow)) except: print("This WebApp was created with an old version of WebApp Manager. Its URL cannot be edited.") with open(path, 'w') as configfile: config.write(configfile, space_around_delimiters=False) def bool_to_string(boolean): if boolean: return "true" else: return "false" def normalize_url(url): (scheme, netloc, path, _, _, _) = urllib.parse.urlparse(url, "http") if not netloc and path: return urllib.parse.urlunparse((scheme, path, "", "", "", "")) return urllib.parse.urlunparse((scheme, netloc, path, "", "", "")) def download_image(root_url: str, link: str) -> Optional[PIL.Image.Image]: if "://" not in link: if link.startswith("/"): link = root_url + link else: link = root_url + "/" + link try: response = requests.get(link, timeout=3) image = PIL.Image.open(BytesIO(response.content)) if image.height > 256: return image.resize((256, 256), PIL.Image.BICUBIC) return image except Exception as e: print(e) print(link) return None def _find_link_favicon(soup, iconformat, url): items = soup.find_all("link", {"rel": iconformat}) for item in items: link = item.get("href") if link: yield link def _find_meta_content(soup, iconformat, url): item = soup.find("meta", {"name": iconformat}) if not item: return link = item.get("content") if link: yield link def _find_property(soup, iconformat, url): items = soup.find_all("meta", {"property": iconformat}) for item in items: link = item.get("content") if link: yield link def _find_url(_soup, iconformat, url): yield iconformat def _find_google_api_favicon(_soup, iconformat, url): url = urllib.parse.quote(url, safe='') #response = requests.get("https://www.google.com/s2/favicons?sz=32&domain=%s" % url, timeout=3) #link = response.url link = "https://www.google.com/s2/favicons?sz=32&domain=%s" % url yield link def download_favicon(url): images = [] url = normalize_url(url) (scheme, netloc, path, _, _, _) = urllib.parse.urlparse(url) root_url = "%s://%s" % (scheme, netloc) api_url = "%s%s" % (netloc, path) # Check HTML and /favicon.ico try: response = requests.get(url, timeout=3) if response.ok: import bs4 soup = bs4.BeautifulSoup(response.content, "html.parser") iconformats = [ ("apple-touch-icon", _find_link_favicon), ("shortcut icon", _find_link_favicon), ("icon", _find_link_favicon), ("msapplication-TileImage", _find_meta_content), ("msapplication-square310x310logo", _find_meta_content), ("msapplication-square150x150logo", _find_meta_content), ("msapplication-square70x70logo", _find_meta_content), ("og:image", _find_property), ("favicon.ico", _find_url), ("google-api", _find_google_api_favicon), ] # icons defined in the HTML for (iconformat, getter) in iconformats: for link in getter(soup, iconformat, api_url): image = download_image(root_url, link) if image is not None: t = tempfile.NamedTemporaryFile(suffix=".png", delete=False) image.save(t.name) images.append([iconformat, image, t.name]) except Exception as e: print(e) images = sorted(images, key = lambda x: x[1].height, reverse=True) return images if __name__ == "__main__": download_favicon(sys.argv[1])