Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
webapp-manager / usr / lib / webapp-manager / common.py
Size: Mime:
#!/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")
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 = range(9)

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.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 "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_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(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, 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)

        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" % _("Web App"))

            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]:
            # 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
            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, browser, url, icon, category, custom_parameters, codename, isolate_profile, navbar, privatewindow):
        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", _("Web App"))
        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):
    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):
    item = soup.find("meta", {"name": iconformat})
    if not item:
        return
    link = item.get("content")
    if link:
        yield link

def _find_property(soup, iconformat):
    items = soup.find_all("meta", {"property": iconformat})
    for item in items:
        link = item.get("content")
        if link:
            yield link

def _find_url(_soup, iconformat):
    yield iconformat


def download_favicon(url):
    images = []
    url = normalize_url(url)
    (scheme, netloc, path, _, _, _) = urllib.parse.urlparse(url)
    root_url = "%s://%s" % (scheme, netloc)

    # 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),
            ]

            # icons defined in the HTML
            for (iconformat, getter) in iconformats:
                for link in getter(soup, iconformat):
                    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])