tis-template-microsoft-store-app icon

Template Microsoft Store App

Silent install package for Template Microsoft Store App

0-77

  • package: tis-template-microsoft-store-app
  • name: Template Microsoft Store App
  • version: 0-77
  • categories: Template
  • maintainer: WAPT Team,Tranquil IT,Joffrey Le Piquet
  • licence: wapt_public
  • locale: all
  • target_os: windows
  • architecture: all
  • signature_date:
  • size: 38.51 Ko

package           : tis-template-microsoft-store-app
version           : 0-77
architecture      : all
section           : base
priority          : optional
name              : Template Microsoft Store App
categories        : Template
maintainer        : WAPT Team,Tranquil IT,Joffrey Le Piquet
description       : Once imported, run the update_package() function to automatically generate your package for a UWP application available on the Microsoft Store
depends           : 
conflicts         : 
maturity          : PROD
locale            : all
target_os         : windows
min_wapt_version  : 2.3
sources           : 
installed_size    : 
impacted_process  : 
description_fr    : Une fois importé, exécutez la fonction update_package() afin de générer automatiquement votre package pour une application UWP disponible sur le Microsoft Store.
description_pl    : Po zaimportowaniu uruchom funkcję update_package(), aby automatycznie wygenerować pakiet dla aplikacji UWP dostępnej w sklepie Microsoft Store
description_de    : Führen Sie nach dem Import die Funktion update_package() aus, um Ihr Paket für eine im Microsoft Store erhältliche UWP-Anwendung automatisch zu generieren
description_es    : Una vez importado, ejecute la función update_package() para generar automáticamente su paquete para una aplicación UWP disponible en Microsoft Store
description_pt    : Uma vez importado, execute a função update_package() para gerar automaticamente o seu pacote para uma aplicação UWP disponível na Microsoft Store
description_it    : Una volta importato, eseguire la funzione update_package() per generare automaticamente il pacchetto per un'applicazione UWP disponibile sul Microsoft Store
description_nl    : Voer na het importeren de functie update_package() uit om automatisch je pakket te genereren voor een UWP-toepassing die beschikbaar is in de Microsoft Store
description_ru    : После импорта запустите функцию update_package() для автоматической генерации пакета для UWP-приложения, доступного в Microsoft Store
audit_schedule    : 
editor            : 
keywords          : 
licence           : wapt_public
homepage          : 
package_uuid      : 6f75afb0-ad0e-40c1-b1c2-f2f698781d60
valid_from        : 
valid_until       : 
forced_install_on : 
changelog         : 
min_os_version    : 10.0
max_os_version    : 
icon_sha256sum    : aea6d0c53867b3d774e670da830d8ef922bd93a4ca37ea565e7bbb8465152983
signer            : Tranquil IT
signer_fingerprint: 8c5127a75392be9cc9afd0dbae1222a673072c308c14d88ab246e23832e8c6bb
signature_date    : 2025-09-06T02:03:00.000000
signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes
signature         : eJe1emJe8wkEaJBJ3SgpFxeRNH5KhCuGJUi3n2cH8rYY+4WBSgFAE+OjFwWRidlokWVUWXr9j2H+xx+tX4Vh/4YAL0h8BxKNfjE5AfpmQMbKgmi7/7l55Y0Oyb1G4VN35HXLEWgbBuZaB27PSEJNyyt2mOlt548p9sVnW0E3/xMdqih65iVUn/gq1iBs0ncdMU0AArVQz+RzjR5syw1UrPZZMPNwRiOqbJuElhqgrKaLNb3ZgkBz9Oc6kj9Wg2UKwTW783TQFEooAQI6kSsLNJBjg8rZUB29kpzZAmE+Aa/b/xC0PHnlkgRigXF1DtDM44kJ7/+2aWJ4y6OX2Q+Cbg==

# -*- coding: utf-8 -*-
from setuphelpers import *


appx_package_name = ""
appx_dir = makepath(programfiles, "WindowsAppsInstallers")


def install():
    # Declare local variables
    bins_dir = control.package.split("-", 1)[1]
    old_appx_bins_dir = makepath(appx_dir, bins_dir)
    appx_bins_dir = makepath(os.getcwd(), bins_dir)

    # Removing sources because they are no longer needed
    if isdir(old_appx_bins_dir):
        print("Removing: %s" % (old_appx_bins_dir))
        remove_tree(old_appx_bins_dir)
    if dir_is_empty(appx_dir):
        print("Removing: %s since it is empty" % (appx_dir))
        remove_tree(appx_dir)

    # Removing binaries of different architectures
    for f in glob.glob(appx_bins_dir + "/*"):
        fname = f.split(os.sep)[-1]
        if control.architecture == "all":
            if "x86" in glob.glob(makepath(appx_bins_dir, f"{appx_package_name}_*"))[0].split(os.sep)[-1]:
                if not "x86" in fname and not "neutral" in fname:
                    remove_file(f)
            else:
                if not get_host_architecture() in fname and not "neutral" in fname:
                    remove_file(f)
        else:
            if not get_host_architecture() in fname and not "neutral" in fname:
                remove_file(f)
    bin_path = glob.glob(makepath(appx_bins_dir, f"{appx_package_name}_*"))[0]
    dependencies_pathes = ",".join([f'"{a}"' for a in glob.glob(makepath(appx_bins_dir, "*")) if not appx_package_name in a])
    add_appx_cmd = f'Add-AppxProvisionedPackage -Online -PackagePath "{bin_path}" -SkipLicense'
    if dependencies_pathes:
        add_appx_cmd += f" -DependencyPackagePath {dependencies_pathes}"

    # Installing the UWP application if needed
    appxprovisionedpackage = run_powershell(f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{appx_package_name}"')
    if appxprovisionedpackage is None:
        remove_appx(appx_package_name, False)
        appxprovisionedpackage = {"Version": "0"}
    elif force:
        uninstall()
    if Version(appxprovisionedpackage["Version"], 4) < Version(control.get_software_version(), 4):
        print(f"Installing: {bin_path.split(os.sep)[-1]} ({control.get_software_version()})")
        killalltasks(ensure_list(control.impacted_process))
        run_powershell(add_appx_cmd, output_format="text")
    else:
        print(f'{appxprovisionedpackage["PackageName"]} is already installed and up-to-date.')


def uninstall():
    print(f"Removing AppX: {appx_package_name}")
    remove_appx(appx_package_name)


def audit():
    # Declaring local variables
    audit_result = "OK"
    audit_version = True
    appxprovisionedpackage = run_powershell(f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{appx_package_name}"')

    # Auditing software
    if appxprovisionedpackage is None:
        print(f"{appx_package_name} is not installed.")
        audit_result = "ERROR"
    elif audit_version:
        if Version(appxprovisionedpackage.get("Version", "0"), 4) < Version(control.get_software_version(), 4):
            print(
                f'{appxprovisionedpackage["PackageName"]} is installed in version: {appxprovisionedpackage["Version"]} instead of: {control.get_software_version()}.'
            )
            audit_result = "WARNING"
        else:
            print(f'{appxprovisionedpackage["PackageName"]} is installed and up-to-date.')
    else:
        print(f'{appxprovisionedpackage["PackageName"]} is installed.')

    return audit_result


def remove_appx(package, default_user=True):
    """Remove Windows AppX package from the computer environment, excluding NonRemovable packages.

    Args:
        package (str): AppX package name. You can use an asterisk (*) as a wildcard.
        default_user (bool): Remove AppX package from the Windows image to prevent installation for new users.

    .. versionadded:: 2.2

    .. versionchanged:: 2.5
        No longer try to remove NonRemovable AppX package

    """
    if running_as_admin() or running_as_system():
        if default_user:
            run_powershell(
                f'Get-AppXProvisionedPackage -Online | Where-Object DisplayName -Like "{package}" | Remove-AppxProvisionedPackage -Online -AllUsers',
                output_format="text",
            )
        run_powershell(
            r'Get-AppxPackage -Name "%s" -AllUsers | Where-Object {{ -not ($_.NonRemovable) }} | Remove-AppxPackage -AllUsers' % package,
            output_format="text",
        )
    else:
        run_powershell(r'Get-AppxPackage -Name "%s" | Where-Object {{ -not ($_.NonRemovable) }} | Remove-AppxPackage' % package, output_format="text")

# -*- coding: utf-8 -*-
from setuphelpers import *

try:
    from setupdevhelpers import *

except:
    from bs4 import BeautifulSoup
import json
import waptguihelper
import html
from xml.dom import minidom
import requests
import json
import re

release_name_map = {"retail": "Retail", "RP": "Release Preview", "WIS": "Insider Slow", "WIF": "Insider Fast"}
release_type = "retail"
release_name = release_name_map[release_type]
arch = "x64"

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import xmltodict

#For debug mode, set as True
debug_mode = False

#To make 5 api requests, set as True / Only use if retrieve binary is bad
loop_enable = False

def update_package():
    # Declaring local variables
    package_updated = False
    proxies = get_proxies()
    
    if not proxies:
        proxies = get_proxies_from_wapt_console()
    store_url = control.sources
    
    if not store_url:
        store_id = waptguihelper.input_dialog(
            "Choice of app", "Enter the windows store app id (foundable in package url: https://apps.microsoft.com/store/apps)", store_url
        ).split("/")[-1]
    else:
        store_id = store_url.split("/")[-1]
    store_id = store_id.split("?")[0]

    url_ms = "https://apps.microsoft.com/store/detail/%s" % store_id
    control.sources = url_ms
    control.save_control_to_wapt()

    package_json = get_json(store_id)

    # Updating package.json ignored_versions list
    if isfile("package.json"):
        package_infos = json_load_file("package.json")
    else:
        package_infos = {"version_prefix": None, "ignored_versions": []}

    ignored_versions = package_infos["ignored_versions"]
    version_prefix = package_infos.get("version_prefix", None)
    
    #Debug print package.json informations
    if debug_mode:
        print("\n\n=== CAREFUL : DEBUG MODE IS ACTIVE ===\n")
        print(f"version prefix : {version_prefix}")
        print(f"ignored versions : {ignored_versions}\n")
    
    #Get files from Microsoft Store API
    print("Retrieving informations from Microsoft Store API, please wait\n")
    all_files_dict = get_all_files(url_ms,proxies)
    
    #Debug print all_files_dict, contains all files from a Store url API request
    if debug_mode:
        all_files_dict_json = json.dumps(all_files_dict,indent=4)
        print("*** Pretty return from get_all_files() ***")
        print(f"all_files_dict : {all_files_dict_json}\n")

	# check setup.py incase the package name doesnt match the installer file
    with open("setup.py", "r") as f:
        for line in f.readlines():
            if line.startswith("appx_package"):
                store_package_name = line.split("=")[1].split('"')[1]
                break
    
    #Find the last binaries or the one matching prefix inside all_files_dict
    bin_selected_dict = ask_app_filename(all_files_dict, store_package_name,ignored_versions=ignored_versions,version_prefix=version_prefix)
    
    #Debug print post filtering to see which files have been selected
    if debug_mode:
        bin_selected_dict_json = json.dumps(bin_selected_dict,indent=4)
        print("*** Selected file from ask_app_file_name() ***")
        print(f"bin_selected_dict : {bin_selected_dict_json}\n")

    #Define binarie variables
    appx_package = bin_selected_dict[0]["bin_name"].split("_")[0]
    bin_selected = bin_selected_dict[0]
    latest_bin = bin_selected["bin_name"]
    version = bin_selected["version"]
    download_url = bin_selected["download_url"]
    package_name = bin_selected["package_name"]
    software_name = bin_selected["software_name"]
    package_arch = bin_selected["package_arch"]
    
    #Download binarie file
    if download_url.split("/")[2].endswith("microsoft.com"):
        if not isfile(latest_bin):
            print("Downloading: %s" % latest_bin)
            wget(download_url, latest_bin, proxies=proxies)
        else:
            print("Binary file version corresponds to online version")
    else:
        print("ERROR: The retrieved url will not download from microsoft's servers")

    # Asking pkg infos if needed and not running as luti
    if not params.get("running_as_luti"):
        if package_arch == "all":
            ask_control_architecture(package_arch)
        ask_control_categories()
        ask_control_package(package_name, "template-microsoft-store", remove_base_files=True)
        ask_control_name(software_name, "Template Microsoft Store")
        # description from microsoft website
        ask_control_description("update_package", get_description(package_json))

    # Changing setup.py appx_package variable
    new_lines = []
    with open("setup.py", "r") as f:
        for line in f.readlines():
            if line.startswith("appx_package"):
                line = 'appx_package_name = "%s"\n' % latest_bin.split("_")[0]
            new_lines.append(line)
    with open("setup.py", "w") as f:
        f.writelines(new_lines)

	#Making dependency dict from downloaded binary
    uwp_app_dict = make_dependency_dict(appx_package)

    if not uwp_app_dict:
        # error(f'{appx_package_name} not found in "uwp_app_dict".\n\n{list(set([ua["bin_name"].split("_")[0] for ua in all_files]))}')
        error(f'"{appx_package}" not found in "uwp_app_dict". This is probably caused by "package_json" filters:\n{package_json}')
    
    #Download each dependency
    newer_uwp_app = download_uwp_dependency(uwp_app_dict,all_files_dict,proxies)
    
    #To make things pretty
    if debug_mode:
        print("\n")
    
    # Changing version of the package
    if Version(version) > Version(control.get_software_version()):
        print("Software version updated (from: %s to: %s)" % (control.get_software_version(), Version(version)))
        package_updated = True
    else:
        print("Software version up-to-date (%s)" % Version(version))

    if debug_mode:
        print("Deleting binary files and exiting")
        for uwp_file in glob.glob(f'{newer_uwp_app["Name"]}*{newer_uwp_app["Version"]}*.*'):
            remove_file(uwp_file)
            return

    control.set_software_version(version)
    control.save_control_to_wapt()

    # Placing binaries in a dir ["appxbundle", "msixbundle", "appx", "msix]
    for f in glob.glob("*.part"):
        remove_file(f)
    bins_dir = control.package.split("-", 1)[1]
    if isdir(bins_dir):
        remove_tree(bins_dir)
    mkdirs(bins_dir)
    for uwp_file in glob.glob(f'{newer_uwp_app["Name"]}*{newer_uwp_app["Version"]}*.*'):
        if not isfile(makepath(bins_dir, uwp_file)):
            shutil.move(uwp_file, bins_dir)
    if uwp_app_dict[newer_uwp_app["FileName"]]["Dependencies"] is not None:
        for l in [glob.glob(a["Name"] + "_*") for a in uwp_app_dict[newer_uwp_app["FileName"]]["Dependencies"]]:
            for f in l:
                if not isfile(makepath(bins_dir, f)):
                    shutil.move(f, bins_dir)

    # Deleting outdated binaries
    remove_outdated_binaries(version, ["appxbundle", "msixbundle", "appx", "msix", "part"], latest_bin)

    # Validating or not update-package-sources
    return package_updated

def get_json(app_id):
    # download json from url and return it as dict
    url = "https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/%s?market=US&locale=en-us&deviceFamily=Windows.Desktop" % app_id
    dict = json.loads(requests.get(url).text)
    return dict

def get_all_files(store_id,proxies):
    all_files = url_generator(
        store_id,
        proxies=proxies
    )

    #Debug print raw dict directly from API
    if debug_mode:
        all_files_json = json.dumps(all_files,indent=4)
        print("*** Raw return from url_generator() ***")
        print(f"all_files : {all_files_json}\n")

    all_files_dict = []
    
    # parse every file name and url in all_files and make a dict:
    for i,download_url in all_files[0].items():
        version = i.split("_")[1]
        bin_name = i.replace("~", "_")
        pkg_splitted = re.split(r"_\d+\.", bin_name)[0]
        package_prefix = control.package.split("-")[0]
        package_name = package_prefix + "-" + pkg_splitted.split("_")[0].replace(".", "-").lower()
        software_name = bin_name.split("_")[0].replace("-", " ").replace(".", " ")
        if "arm64" in bin_name:
            package_arch = "arm64"
        elif "arm" in bin_name:
            package_arch = "arm"
        elif "x64" in bin_name:
            package_arch = "x64"
        # elif "x86" in bin_name:
        #     package_arch = "all"  # not x86 since it may be required for x64
        else:
            package_arch = "all"

        file_dict = {
            "version": version,
            "bin_name": bin_name,
            "package_name": package_name,
            "software_name": software_name,
            "package_arch": control.architecture,
            "package_arch": package_arch,
            "download_url": download_url,
        }
        all_files_dict.append(file_dict)
    return all_files_dict

def url_generator(url,proxies):
    def uwp_gen(data_list):

        cat_id = data_list["WuCategoryId"]
        main_file_name = data_list["PackageFamilyName"].split("_")[0]
        release_type = "Retail"

        #Ask Microsoft API for a cookie
        with open(rf"{basedir}\data\xml\GetCookie.xml", "r") as f:
            cookie_content = f.read()

        out = requests.post(
            "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
            data=cookie_content,
            headers={"Content-Type": "application/soap+xml; charset=utf-8"},
            proxies=proxies,
            verify=False
        ).text
        doc = minidom.parseString(out)

        cookie = doc.getElementsByTagName("EncryptedData")[0].firstChild.nodeValue

        with open(rf"{basedir}\data\xml\WUIDRequest.xml", "r") as f:
            cat_id_content = f.read().format(cookie, cat_id, release_type)

        out = requests.post(
            "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
            data=cat_id_content,
            headers={"Content-Type": "application/soap+xml; charset=utf-8"},proxies=proxies,verify=False).text

        doc = minidom.parseString(html.unescape(out))
        
        if loop_enable:
            #To avoid picking wrong miror, make 5 requests and merge responses
            req = 0
            for req in range(4):
                out2 = requests.post(
                    "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx",
                    data=cat_id_content,
                    headers={"Content-Type": "application/soap+xml; charset=utf-8"},proxies=proxies,verify=False).text

                doc2 = minidom.parseString(html.unescape(out2))
                imp = doc.importNode(doc2.childNodes[0],True)
                doc.childNodes[0].appendChild(imp)
                req += 1
            
        filenames = {}
        for node in doc.getElementsByTagName("Files"):
            try:
                filenames[
                    node.parentNode.parentNode.getElementsByTagName("ID")[0].firstChild.nodeValue
                ] = (
                    f"{node.firstChild.attributes['InstallerSpecificIdentifier'].value}_{node.firstChild.attributes['FileName'].value}",
                    node.firstChild.attributes["Modified"].value,
                )
            except KeyError:
                continue
        if not filenames:
            raise Exception("server returned an empty list")

        identities = {}
        name_modified = {}
        for node in doc.getElementsByTagName("SecuredFragment"):
            try:
                file_name, modifed = filenames[
                    node.parentNode.parentNode.parentNode.getElementsByTagName("ID")[0].firstChild.nodeValue
                ]
                update_identity = node.parentNode.parentNode.firstChild
                name_modified[file_name] = modifed
                identities[file_name] = (
                    update_identity.attributes["UpdateID"].value,
                    update_identity.attributes["RevisionNumber"].value,
                )
            except KeyError:
                continue
        
        api_dict = {value: identities[value] for value in name_modified}
        
        #Debug print raw dict directly from API
        if debug_mode:
            api_dict_json = json.dumps(api_dict,indent=4)
            print("*** First API Return from url_generator() ***")
            print(f"api_dict : {api_dict_json}\n")

        with open(rf"{basedir}\data\xml\FE3FileUrl.xml", "r") as f:
            file_content = f.read()

        file_dict = {}

        for file_name, (updateid, revisionnumber) in api_dict.items():

            out = requests.post(
                "https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx/secured",
                data=file_content.format(updateid, revisionnumber, release_type),
                headers={"Content-Type": "application/soap+xml; charset=utf-8"},
                proxies=proxies,
                verify=False
            ).text
            doc = minidom.parseString(out)
            for i in doc.getElementsByTagName("FileLocation"):
                url = i.getElementsByTagName("Url")[0].firstChild.nodeValue
                if len(url) != 99:
                    file_dict[file_name] = url


        if len(file_dict) != len(api_dict):
            raise Exception("server returned an incomplete list")

        return file_dict, main_file_name

    def non_uwp_gen(product_id):

        api = f"https://storeedgefd.dsx.mp.microsoft.com/v9.0/packageManifests//{product_id}?market=US&locale=en-us&deviceFamily=Windows.Desktop"

        data = requests.get(api,proxies=proxies).text
        datas = json.loads(data)

        if not datas.get("Data"):
            raise Exception("server returned an empty list")

        file_name = datas["Data"]["Versions"][0]["DefaultLocale"]["PackageName"]
        installer_list = datas["Data"]["Versions"][0]["Installers"]

        download_data = set((d["Architecture"], d["InstallerLocale"], d["InstallerType"], d["InstallerUrl"]) for d in installer_list)
        curr_arch = os_arc()
        download_data = list(download_data)

        arch, locale, installer_type, url = download_data[0]
        if len(download_data) > 1:
            for data in download_data[1:]:
                if arch not in ("neutral", curr_arch) and data[0] in ("neutral", curr_arch):
                    arch, locale, installer_type, url = data
                elif data[0] == arch and data[1] != locale and ("us" in data[1] or "en" in data[1]):
                    locale, installer_type, url = data[1], data[2], data[3]
                    break

        main_file_name = clean_name(file_name) + "." + installer_type
        file_dict = {main_file_name: url}
        return file_dict, [main_file_name], main_file_name, False

    def clean_name(badname):
        name = "".join([(i if (64 < ord(i) < 91 or 96 < ord(i) < 123) else "") for i in badname])
        return name.lower()

    pattern = re.compile(r".+\/([^\/\?]+)(?:\?|$)")
    matches = pattern.search(str(url))
    if not matches:
        raise Exception("No Data Found: --> [You Selected Wrong Page, Try Again!]")

    product_id = matches.group(1)
    details_api = f"https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/{product_id}?market=US&locale=en-us&deviceFamily=Windows.Desktop"
    data = requests.get(details_api,proxies=proxies).text
    response = json.loads(data, object_hook=lambda obj: {k: json.loads(v) if k == "FulfillmentData" else v for k, v in obj.items()})

    if not response.get("Payload"):
        raise Exception("No Data Found: --> [You Selected Wrong Page, Try Again!]")

    response_data = response["Payload"]["Skus"][0]
    data_list = response_data.get("FulfillmentData")

    if data_list:
        return uwp_gen(data_list)
    else:
        return non_uwp_gen(product_id)

def ask_app_filename(all_files_dict, store_package_name, ignored_versions=[], version_prefix=None):
    # input files dict and a  store packa_name if it exist, if empty selct automatically the proper file if not ask user to select it
    if "template-microsoft-store" in control.package:
        selected = waptguihelper.grid_dialog(
            "Please select the proper file",
            json.dumps(all_files_dict),
            waptguihelper.GRT_SELECTED,
            '{"columns":[{"propertyname":"version","datatype":"String","required":false,"readonly":false,"width":130},{"propertyname":"bin_name","datatype":"String","required":false,"readonly":false,"width":420},{"propertyname":"package_name","datatype":"String","required":false,"readonly":false,"width":190},{"propertyname":"software_name","datatype":"String","required":false,"readonly":false,"width":172},{"propertyname":"package_arch","datatype":"String","required":false,"readonly":false,"width":88},{"propertyname":"download_url","datatype":"String","required":false,"readonly":false,"width":1472}]}',
        )
    else:
        selected = [a for a in all_files_dict if (a["package_arch"] in ensure_list(control.architecture) or control.architecture == "all" or a["package_arch"] == "all")]
        if len(selected) > 1:
            if version_prefix:
                target_version = None
                for app_file in all_files_dict:
                    latest_bin = app_file["bin_name"]
                    if not latest_bin.startswith(f"{store_package_name}_"):  # ignore other files
                        continue
                    if latest_bin.split(".")[-1].lower().startswith("e"):  # ignore encrypted UWP apps
                        continue
                    if version_prefix is not None and Version(app_file["version"], len(version_prefix.split("."))) == Version(
                        version_prefix, len(version_prefix.split(".")) and target_version < Version(app_file["version"]) #Find the file matching version_prefix
                    ):
                        target_version=app_file["version"]
                        break
                selected = [a for a in selected if target_version == a["version"] and store_package_name in a["bin_name"]]
            else:
                higher_version = "0"
                for a in all_files_dict:
                    if any(
                        Version(a["version"], len(ignored_version.split("."))) == Version(ignored_version, len(ignored_version.split(".")))
                        for ignored_version in ignored_versions #ignore files in ignored_versions
                    ):
                        continue
                    if Version(higher_version) < Version(a["version"]) and store_package_name in a["bin_name"]: # Select the higher version
                        higher_version = a["version"]
                        continue
                selected = [a for a in selected if higher_version == a["version"] and store_package_name in a["bin_name"]]

    return selected

def ask_control_categories():
    """Requesting that the user supply package categories for the control.categories field if empty or Template is selected"""

    if control.categories == "Template":
        categories = waptguihelper.grid_dialog(
            "Select package categories",
            [
                "Internet",
                "Utilities",
                "Messaging",
                "Security",
                "System and network",
                "Media",
                "Development",
                "Office",
                "Drivers",
                "Education",
                "Configuration",
                "CAD",
                "Template",
                "Dependencies",
                "Extensions",
            ],
            waptguihelper.GRT_SELECTED,
        )
        if categories:
            control.categories = ",".join([a["unknown"] for a in categories])
        else:
            control.categories = ""
        control.save_control_to_wapt()
    return control.categories

def ask_control_architecture(package_arch):
    """Requesting that the user supply package architecture for the control.architecture field if empty or Template is selected"""

    if control.categories == "Template":
        architecture = waptguihelper.grid_dialog(
            f"Package Architecture available : {package_arch}",
            [
                "all",
                "x64",
                "x86",
                "arm",
                "arm64",
            ],
            waptguihelper.GRT_SELECTED,
        )
        if architecture:
            control.architecture = ",".join([a["unknown"] for a in architecture])
        else:
            control.architecture = package_arch
        control.save_control_to_wapt()
    return control.architecture

def ask_control_package(control_package, conditionnal_package_name=None, remove_base_files=False):
    """Requesting that the user provide a package name to be entered into the control.package field, and offering the possibility of removing the base files (icon.png and changelog.txt) for template package usage

    Args:
        control_package             (str)   : prefilled control_package (default: actual control_package)
        conditionnal_package_name   (str)   : only ask when the control.package contains conditionnal_package_name (default: always ask for control_package)
        remove_base_files           (bool)  : removes base files if parameter is True and conditionnal_package_name is provided (default: False)
    """
    if conditionnal_package_name is None or conditionnal_package_name in control.package:
        control.package = waptguihelper.input_dialog(control.package, "You can redefine the package name", control_package)
        control.save_control_to_wapt()

    # Removing template files
    if conditionnal_package_name in control.package and remove_base_files:
        if isfile("WAPT\\changelog.txt"):
            remove_file("WAPT\\changelog.txt")
        if isfile("WAPT\\icon.png"):
            remove_file("WAPT\\icon.png")
    return control.package

def ask_control_name(control_name, conditionnal_package_name=None):
    """Requesting that the user provide a package name to be entered into control.name field

    Args:
        control_name                (str)   : prefilled control_name (default: control.name)
        conditionnal_package_name   (str)   : only ask when the control.name contains conditionnal_package_name (default: always ask for control_name)
    """
    if conditionnal_package_name is None or conditionnal_package_name in control.name:
        control.name = waptguihelper.input_dialog(control.name, "You can redefine the name for the self-service", control_name)
        control.save_control_to_wapt()
    return control.name

def ask_control_description(blank_str=None, description_from_store=""):
    """Requesting that the user supply package description for the control.description field

    Args:
        blank_str   (str): The description will be cleared if it includes the specified string to avoid using the template description (default: do not clear description)
    """
    if not control.description:
        control.description = ask_dialog("Description", "Please fill the description", description_from_store)
        control.save_control_to_wapt()
    if blank_str is not None and blank_str in control.description:
        control.description = ""
        control.save_control_to_wapt()
    return control.description

def ask_dialog(title, text, default="", stay_on_top=False):
    """This function opens a dialog box with a action input"""
    # - Title: the title for the dialog
    # - Text: indicates which value the user should type
    # - Default: the default value, if the user do not want to type any
    # Optional:
    # - StayOnTop: indicates if the form will always stay on top - default is False
    return waptguihelper.input_dialog(title, text, default, stay_on_top)

def get_description(dict):
    # get the description from the dict
    return dict["Payload"]["Skus"][0]["Description"]

def make_dependency_dict(appx_package_name):

    #Make dependendy dict directly from binary file
    ms_app_db = {}

    for ms_app_file in (
        glob.glob(f"{appx_package_name}*.appxbundle")
        + glob.glob(f"{appx_package_name}*.msixbundle")
        + glob.glob(f"{appx_package_name}*.appx")
        + glob.glob(f"{appx_package_name}*.msix")
    ):
        data_dict = {}
        before_dependencies = []
        sub_dependencies = []
        dependency_data_dict = None

        manifest = None
        bundle_manifest = None

        manifest_path = makepath(basedir,"AppxManifest.xml")
        # Get AppxManifest, contains dependencies list
        unzip_with_7zip(ms_app_file, ".", "AppxManifest.xml", False)
        
        if isfile(manifest_path):
            manifest = manifest_path
        
        bundle_manifest_path = makepath(basedir,"AppxBundleManifest.xml")
        # Get AppxBundleManifest, contains dependencies list
        unzip_with_7zip(ms_app_file, ".", "AppxMetadata\\AppxBundleManifest.xml", False)
        
        if isfile(bundle_manifest_path):
            bundle_manifest = bundle_manifest_path
        
        ms_app_info = {
            "FileName": ms_app_file,
        }

        if not manifest:
            if bundle_manifest:
                with open(bundle_manifest, encoding="utf8") as xml_file:
                    data_dict = xmltodict.parse(xml_file.read(), attr_prefix="")

                if type(data_dict["Bundle"]["Packages"]["Package"]) == dict:
                    data_dict["Bundle"]["Packages"]["Package"] = [dict(data_dict["Bundle"]["Packages"]["Package"])]

                for app_pkg in list(data_dict["Bundle"]["Packages"]["Package"]):
                    if app_pkg["Type"] == "application":
                        before_dependency_info = {
                            "FileName": app_pkg["FileName"],
                            "Version": app_pkg["Version"],
                            "Architecture": app_pkg["Architecture"],
                        }
                        before_dependencies.append(before_dependency_info)

                if before_dependencies:
                    for dependency in before_dependencies:
                        unzip(
                            ms_app_file,
                            ".",
                            makepath(
                                dependency["FileName"],
                            ),
                            False,
                        )
                        unzip(dependency["FileName"], ".", makepath("AppxManifest.xml"), False)
                        manifest = manifest_path = makepath(basedir,"AppxManifest.xml")
                        if isfile(dependency["FileName"]):
                            remove_file(dependency["FileName"])
                        with open(manifest, encoding="utf8") as xml_file:
                            dependency_data_dict = xmltodict.parse(xml_file.read(), attr_prefix="")

                    if dependency_data_dict["Package"].get("Dependencies"):
                        if dependency_data_dict["Package"]["Dependencies"].get("PackageDependency"):
                            sub_dependencies = list(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"])
                            if type(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]) == dict:
                                sub_dependencies = [dict(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"])]
                            if type(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]) == list:
                                sub_dependencies = dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]
                        if dependency_data_dict["Package"]["Dependencies"].get("TargetDeviceFamily"):
                            if not "TargetDeviceFamily" in ms_app_info:
                                if type(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]) == dict:
                                    ms_app_info["TargetDeviceFamily"] = [dict(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"])]
                                if type(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]) == list:
                                    ms_app_info["TargetDeviceFamily"] = dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]
            else:
                error("nothing to parse")
        else:
            unzip_with_7zip(ms_app_file, ".", makepath("AppxManifest.xml"), False)
            manifest = manifest_path = makepath(basedir,"AppxManifest.xml")
            with open(manifest, encoding="utf8") as xml_file:
                dependency_data_dict = xmltodict.parse(xml_file.read(), attr_prefix="")

        if dependency_data_dict["Package"].get("Dependencies"):
            if "PackageDependency" in dependency_data_dict["Package"]["Dependencies"]:
                sub_dependencies = list(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"])
                if type(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]) == dict:
                    sub_dependencies = [dict(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"])]
                if type(dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]) == list:
                    sub_dependencies = dependency_data_dict["Package"]["Dependencies"]["PackageDependency"]
            if "TargetDeviceFamily" in dependency_data_dict["Package"]["Dependencies"]:
                if not "TargetDeviceFamily" in ms_app_info:
                    if type(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]) == dict:
                        ms_app_info["TargetDeviceFamily"] = [dict(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"])]
                    if type(dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]) == list:
                        ms_app_info["TargetDeviceFamily"] = dependency_data_dict["Package"]["Dependencies"]["TargetDeviceFamily"]
        else:
            ms_app_info.update({"Dependencies": None})

        if ms_app_info.get("TargetDeviceFamily"):
            ms_app_info.update(
                {
                    "MinVersion": ms_app_info["TargetDeviceFamily"][0]["MinVersion"],
                    "MaxVersionTested": ms_app_info["TargetDeviceFamily"][0]["MaxVersionTested"],
                }
            )
        elif dependency_data_dict is not None and dependency_data_dict["Package"].get("Prerequisites"):
            ms_app_info.update(
                {
                    "MinVersion": dependency_data_dict["Package"]["Prerequisites"]["OSMinVersion"],
                    "MaxVersionTested": dependency_data_dict["Package"]["Prerequisites"]["OSMaxVersionTested"],
                }
            )
        else:
            ms_app_info.update({"TargetDeviceFamily": None})

        if data_dict:
            ms_app_info.update(data_dict["Bundle"]["Identity"])
        else:
            ms_app_info.update(dependency_data_dict["Package"]["Identity"])
        if "ProcessorArchitecture" in ms_app_info:
            ms_app_info.update({"Architecture": ms_app_info["ProcessorArchitecture"]})
        if before_dependencies:
            ms_app_info["b4:Dependencies"] = before_dependencies
        else:
            ms_app_info.update({"b4:Dependencies": None})

        if sub_dependencies:
            ms_app_info["Dependencies"] = sub_dependencies
        else:
            ms_app_info.update({"Dependencies": None})

        ms_app_db[ms_app_file] = ms_app_info

        for xml_file in glob.glob("AppxManifest.xml") + glob.glob("AppxBundleManifest.xml"):
            remove_file(xml_file)

    return ms_app_db

def get_newer_uwp_depency(dependency_dict, all_files_dict, version_prefix=None, min_version=None):
    """Returns a list of the bin_name of the latest required dependencies"""
    newer_version = "0"

    for uwp_app in all_files_dict:
        if not uwp_app["bin_name"].startswith(f'{dependency_dict["Name"]}_'):
            continue
        if version_prefix is not None and Version(uwp_app["version"], len(version_prefix.split("."))) != Version(
            version_prefix, len(version_prefix.split("."))
        ):
            continue

        if Version(newer_version) < Version(uwp_app["version"]):
            newer_uwp_depency = uwp_app
            newer_version = uwp_app["version"]

    return newer_uwp_depency["bin_name"].split(newer_version + "_")[0] + newer_version + "_"

def download_uwp_dependency(uwp_app_dict,all_files_dict,proxies):

    #Return first dict from list of dict
    newer_uwp_app = next(iter(uwp_app_dict.values()))

    # Downloading dependency files
    dependencies_to_download = []
    if newer_uwp_app["Dependencies"]:
        for dependency in newer_uwp_app["Dependencies"]:
            #Find in all_files_dict, dependency needed in binary
            dependencies_to_download.append(get_newer_uwp_depency(dependency, all_files_dict, min_version=dependency["MinVersion"]))
    if dependencies_to_download:
        
        #Trigger warning that nothing will be download
        if debug_mode:
            print("\nNO DOWNLOADED DEPENDENCY - ONLY PRINTING\n")
        
        for dependency_file in all_files_dict:
            latest_bin = dependency_file["bin_name"]
            if not True in [d in latest_bin for d in dependencies_to_download]:  # ignore unecessary dependencies
                continue
            if not "all" in ensure_list(control.architecture):
                if not get_uwp_filename_arch(latest_bin) in ensure_list(control.architecture) and not "neutral" in latest_bin:
                    if isfile(latest_bin):
                        remove_file(latest_bin)
                    continue
            if latest_bin.split(".")[-1].lower().startswith("e"):  # ignore encrypted uwp apps
                continue

            #Only printing the dependencie without downloading
            if debug_mode:
                print(f"Dependencie to download : {latest_bin}")
                continue
            
            download_url = dependency_file["download_url"]
            if download_url.split("/")[2].endswith("microsoft.com"):
                if not isfile(latest_bin):
                    print("Downloading: %s" % latest_bin)
                    wget(download_url, latest_bin, proxies=proxies)
                else:
                    print("Binary is present: %s" % latest_bin)
            else:
                error("ERROR: The retrieved URL will not download from microsoft's servers")
    
    return newer_uwp_app

def get_uwp_filename_arch(appx_filename, appx_package_name=None):

    """
    Returns the architecture of a Universal Windows Platform (UWP) app based on the provided 'appx_filename'
    and optionally, the 'appx_package_name'.

    Args:
        appx_filename (str): The filename of the UWP app package.
        appx_package_name (str, optional): The package name of the UWP app. Defaults to None.

    Returns:
        str: The architecture of the UWP app, which can be "x64", "arm64", "x86", or "all" (if no specific
        architecture is detected).
    """
    if not appx_package_name:
        appx_package_name = None
    if len(glob.glob(f'{appx_package_name}*{appx_filename.split("_")[1]}*')) > 1:
        pass
    elif "arm64" in appx_filename:
        return "arm64"
    elif "arm" in appx_filename:
        return "arm"
    elif "x64" in appx_filename:
        return "x64"
    if appx_filename is not None and "x86" in appx_filename and (appx_package_name is None or not appx_package_name in appx_filename):
        return "x86"

    return "all"

def os_arc():
    machine = control.architecture
    if machine.endswith("arm64"):
        return "arm64"
    if machine.endswith("64"):
        return "x64"
    if machine.endswith("32") or machine.endswith("86"):
        return "x86"
    else:
        return "arm"

38d056ab130f7bf7c481c12636a4e9959de36561d3dfcbe54c6e3571bc0c1dc3 : WAPT/certificate.crt
3a6df07709510e8084cd91c49cc9f6b9ea7a96856e3625465fe4a64da84f5814 : WAPT/changelog.txt
82636fe2c2508d0c65c720edeee4e9ff390808ea7aa833a402cb7cc193c99ee3 : WAPT/control
aea6d0c53867b3d774e670da830d8ef922bd93a4ca37ea565e7bbb8465152983 : WAPT/icon.png
87370426807b83b7916c4f43942cd9bf71b1eeaf3b44728d241efe107927e3a2 : data/xml/FE3FileUrl.xml
2d3dd983c9d83c2464afaa85ab49ad5c78a537a131ec61e0c32f6b446bed4f55 : data/xml/GetCookie.xml
f8a4681fbeafb4ddcaac37b406374143900d8885b59ba7a7a0ec782d79bacd9b : data/xml/WUIDRequest.xml
97a3de07621c14b9c1121749522b427fda9d991592cbb06a4a0a7b57283c462d : luti.json
bd43e5749147a75dbd658cac066506657073da0ee363e93418ed66427dae1bb6 : package.json
34bb5a9a6e1ad8edaa45b965f47cfedd7eee0c17ac21540518d2afb05f08c11e : setup.py
c802d056a635d7c09e516c827ce4f49551b98844183adf0aa5896ea5bdfd2a03 : setupdevhelpers.py
73a48df1936ca29d6843d8de319465926e5b23e685b3bc15f65a3346d66f6bf7 : update_package.py
364e01205533a0c6907fb9e8a8f70353985ae81ead35b71aade9a4253184f847 : xmltodict.py

0-74
==
Add possibilities to make multiple requests to get good version if some mirrors are bad
Improve test if target version specified

0-73
==
Add debug_mode to print informations

0-71
==
Rework to use Microsoft API

0-68
===
Add compatibility with .msixbundle not seen as zipfile

0-64
===
if the package is not found in the uwp_app_dict an error is returned.
avoid crash with Add-AppxProvisionedPackage when AppxPackage only is already up-to-date.


0-62
===
"tis-template-microsoft-store-app": changed installation method from "Add-AppxPackage" to "Add-AppxProvisionedPackage". Whole setup.py have been reworked.
"tis-template-microsoft-store-dependency": is now used to install Dependencies/DLC/Add-on and stay based on "Add-AppxPackage" installation method.
now autocomplete control.impacted_process and control.installed_size
fix neutral Architecture
fix some crash in control informations asked once
proxy workarounds
fix control.name fetched from page


0-52
===
improve compatibility with Windows 8.x apps
improve complete_control_*() functions
includes xmltodict instead of running "waptpython -m pip download xmltodict"
now avoiding downloads loop and avoid keeping sources of other apps
check version before checking files in session_setup
implement version_prefix to select a preferred version
architecture is:
    - no longer forced on update
    - pre-populate multi-architectures when relevant
    - only compatible architecture dependencies are now downloaded


0-47
===
now, you need to select the app version that you want to package, which enables you to:
    - Download only useful files without editing the update_package.py file.
    - Create the package for both Windows 11 and Windows 10 without editing the control file.
    - Automatically define ignored_versions saved in the package.json file.
handle multi-architecture packages (only works on min_wapt_version: 2.4).
min_wapt_version: 2.3
adding remove_sources feature but do not using it by default since it may prevent multi-user installations.
now asking if user wanna change control.max_os_version instead of only printing it
full audit
by default control_max_os_version if not filled (safer)


0-43
===
fix get_newer_uwp_app and adding multiple options : (version_prefix, min_version, max_version)
create set_control_os_version_uwp_app function
now only download latest necessary dependencies
fix packages where control.architecture != "all"
smarter descriptions editing
min_wapt_version  : 2.2.3
should now works for Windows 8.1+ apps
fix update_package when files without extensions have been detected
fix update_package when no dependencies are required
to avoid unnecessary downloads you can now use download_app_files(ignored_versions=["2017", "2018", "2019", "2020", "2020.1"])
better control completion
setupdevhelpers is now shipped with advanced functions to reduce code duplication
only complete_control once


0-32
===
now includes dependencies, so the tis-template-microsoft-store-dependency part is no longer needed
auto update icon.png
auto update control.min_os_version
smarter popup to ask for appx_package_name
handle "Windows.Desktop" and "Windows.Universal" TargetDeviceFamily
removes encrypted uwp apps
auto update control.max_os_version if relevant ; max_os_version fiabilized ; Ignore end-of-life Windows versions by default


0-25
===
fix rare case of incorrect glob.glob
better handling of binary suppression


0-24
===
fix silent mode
more information shown in grid_dialog
update_package() was inccorect for some architecture
prefix was forced to tis
now using full AppxPackage Name for dependencies
better handling of remove_outdated_binaries and add_depends
improve used regex
using functions for some popups, functions return associated str
more default actions on popups
fix update_package for 2.3 with setupdevhelpers import
fix package name change, and depencies warning not to block luti build
warn the end-user that dependencies are required and providing instructions on how to acquire and deploy them


0-12
===
asking for categories
asking for description
adding dependencies
changing setup.py appx_package variable
smart update_package() it do not popup anything to download updates
using Microsoft_Store_Fluent_Design_icon
remove_outdated_binaries handle extensions
removing template files when used
allow_remove option
specify arch
min_wapt_version  : 2.2
ask to change package_name
fix re.split
fix multi download on neutral apps
only ask once for description