tis-template-microsoft-store-app icon

Template Microsoft Store App

Silent install package for Template Microsoft Store App

0-88
Template
Template

tis-template-microsoft-store-app

Template package to create and maintain Microsoft Store application packages with WAPT.

Installation Procedure

This package serves as a template for creating WAPT packages that automatically retrieve, version, and package applications available in the Microsoft Store (UWP / MSIX / APPX).

Usage Procedure

Step 1 – Download the package

Download the tis-template-microsoft-store-app package to your local WAPT console

Step 2 – Launch the Update Script

Open WAPT Console and right-click on the package → Launch update_package.

This script will guide you through the creation of your Microsoft Store app package.

Interactive Questions (Asked by update_package.py)

During the update process, several dialogs will appear to customize your package:

Step Question / Dialog Description

1 App ID input Enter the Microsoft Store app ID (ex: https://apps.microsoft.com/store/detail/<APP_ID>)

2 Package selection A list of available binaries is shown. Select the correct .appx / .msixbundle version.

3 Architecture selection If multiple architectures exist (x64, arm64, etc.), select the one to include.

4 Category selection Choose one or more WAPT categories (Internet, Office, Media, etc.).

5 Package name You can rename the control.package value (default: auto-generated from app name).

6 Display name Define the control.name shown in Self-Service.

7 Description You can edit or accept the pre-filled description (retrieved from Microsoft Store).

Explanation Video

You can follow this video for more explanation:

  • package: tis-template-microsoft-store-app
  • name: Template Microsoft Store App
  • version: 0-88
  • categories: Template
  • maintainer: WAPT Team,Tranquil IT,Jimmy Pelé
  • licence: wapt_public
  • locale: all
  • target_os: windows
  • architecture: all
  • signature_date:
  • size: 40.31 Ko

package           : tis-template-microsoft-store-app
version           : 0-88
architecture      : all
section           : base
priority          : optional
name              : Template Microsoft Store App
categories        : Template
maintainer        : WAPT Team,Tranquil IT,Jimmy Pelé
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      : 73cf4d9f-485a-426e-b1ce-75b277e0ac05
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    : 2026-03-26T19:00:30.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         : PEmYX2jwn3Z3wGK4nStJXI2pK4Mq0buStmYHb46z+C8I4ApION7FXa9EXC/7EdAl5gCrg1f7Is23Jm+LenPvtkUlUuEzi51OVQRto1p+azQDFCeyn2xgw4BqjTdLWGbJfHm4P6ZMVEwocWYLdNMNqVrA2dIdz5vjRZPflgY+mh1k14pHYaK4WqVIPi4xCXYdmXQKClnOdafaOthUf+e8GHebYRixS2+mjxAJS6ExD8iC6JWcD119KXqo5vjwnjax1jQ4WDBxRhwEH2aBAjnlJ70q/dF2eGUNkP003cfC2QtTLdeaQHEN1/IKGQzld1bz1J4KdNn5mD5Wz9CCvm7stg==

# -*- 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)
    shared_appx_bins_dir = makepath(appx_dir, 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 isdir(appx_dir) and 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 "x86" not in fname and "neutral" not in fname:
                    remove_file(f)
            else:
                if get_host_architecture() not in fname and "neutral" not in fname:
                    remove_file(f)
        else:
            if get_host_architecture() not in fname and "neutral" not in fname:
                remove_file(f)

    bin_path = glob.glob(makepath(appx_bins_dir, f"{appx_package_name}_*"))[0]
    dependencies_paths = [a for a in glob.glob(makepath(appx_bins_dir, "*")) if appx_package_name not in a]

    is_emsixbundle = bin_path.lower().endswith(".emsixbundle")

    # 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"Preparing installation: {bin_path.split(os.sep)[-1]} ({control.get_software_version()})")
        killalltasks(ensure_list(control.impacted_process))

        # Cas EMSIXBUNDLE :
        # - on copie le bundle dans un emplacement accessible à tous
        # - on installe les dépendances si possible en install()
        # - le bundle principal sera installé dans session_setup()
        if is_emsixbundle:
            if not isdir(shared_appx_bins_dir):
                mkdirs(shared_appx_bins_dir)

            shared_bin_path = makepath(shared_appx_bins_dir, os.path.basename(bin_path))
            print(f"Copying EMSIX bundle to: {shared_bin_path}")
            filecopyto(bin_path, shared_bin_path)

            # Copier aussi les dépendances pour session_setup au cas où elles seraient utiles
            for dep in dependencies_paths:
                dep_target = makepath(shared_appx_bins_dir, os.path.basename(dep))
                print(f"Copying dependency to: {dep_target}")
                filecopyto(dep, dep_target)

            # Installer les dépendances en machine si possible
            for dep in dependencies_paths:
                dep_name = os.path.basename(dep)
                if dep.lower().endswith((".appx", ".msix", ".appxbundle", ".msixbundle")):
                    print(f"Installing dependency: {dep_name}")
                    run_powershell(
                        f'Add-AppxProvisionedPackage -Online -PackagePath "{dep}" -SkipLicense',
                        output_format="text",
                    )
                else:
                    print(f"Skipping dependency (unsupported for install phase): {dep_name}")

        else:
            dependencies_pathes = ",".join([f'"{a}"' for a in dependencies_paths])
            add_appx_cmd = f'Add-AppxProvisionedPackage -Online -PackagePath "{bin_path}" -SkipLicense'
            if dependencies_pathes:
                add_appx_cmd += f" -DependencyPackagePath {dependencies_pathes}"

            print(f"Installing: {bin_path.split(os.sep)[-1]} ({control.get_software_version()})")
            run_powershell(add_appx_cmd, output_format="text")
    else:
        print(f'{appxprovisionedpackage["PackageName"]} is already installed and up-to-date.')


def session_setup():
    bins_dir = control.package.split("-", 1)[1]
    shared_appx_bins_dir = makepath(appx_dir, bins_dir)

    candidates = glob.glob(makepath(shared_appx_bins_dir, f"{appx_package_name}_*"))
    if not candidates:
        print(f"No session package found for {appx_package_name} in {shared_appx_bins_dir}")
        return

    bin_path = candidates[0]

    if bin_path.lower().endswith(".emsixbundle"):
        print(f"Installing EMSIX bundle in session: {bin_path}")
        run_powershell(f'Add-AppPackage "{bin_path}"', output_format="text")


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

    bins_dir = control.package.split("-", 1)[1]
    shared_appx_bins_dir = makepath(appx_dir, bins_dir)
    if isdir(shared_appx_bins_dir):
        print(f"Removing cached installer directory: {shared_appx_bins_dir}")
        remove_tree(shared_appx_bins_dir)

    if isdir(appx_dir) and dir_is_empty(appx_dir):
        print("Removing: %s since it is empty" % appx_dir)
        remove_tree(appx_dir)


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",
        )

def session_cleanup():
    print(f"Removing AppX from session: {appx_package_name}")
    remove_appx(appx_package_name, default_user=False)

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

try:
    from setupdevhelpers import *
except:
    pass

import json
import waptguihelper
import html
from xml.dom import minidom
import requests
import re
from datetime import datetime

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"

# 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


download_all_dependencies = True

REQUEST_TIMEOUT = 60


def http_get(url, proxies=None, verify=True, **kwargs):
    return requests.get(
        url,
        proxies=proxies,
        verify=verify,
        timeout=REQUEST_TIMEOUT,
        **kwargs,
    )


def http_post(url, data=None, headers=None, proxies=None, verify=True, **kwargs):
    return requests.post(
        url,
        data=data,
        headers=headers,
        proxies=proxies,
        verify=verify,
        timeout=REQUEST_TIMEOUT,
        **kwargs,
    )


def parse_iso_datetime(iso_str):
    if not iso_str:
        return datetime.min

    try:
        value = iso_str.strip()
        if value.endswith("Z"):
            value = value[:-1] + "+00:00"

        # Normalize too many fractional second digits if present
        m = re.match(r"(.+\.\d{6})\d+(.*)$", value)
        if m:
            value = m.group(1) + m.group(2)

        return datetime.fromisoformat(value)
    except Exception:
        return datetime.min


def get_setup_appx_package_name():
    if not isfile("setup.py"):
        return None

    with open("setup.py", "r", encoding="utf-8") as f:
        for line in f.readlines():
            m = re.match(r'\s*appx_package_name\s*=\s*["\']([^"\']+)["\']', line)
            if m:
                return m.group(1)

    return None


def set_setup_appx_package_name(appx_package_name):
    if not isfile("setup.py"):
        return

    new_lines = []
    found = False

    with open("setup.py", "r", encoding="utf-8") as f:
        for line in f.readlines():
            if re.match(r'\s*appx_package_name\s*=', line):
                line = 'appx_package_name = "%s"\n' % appx_package_name
                found = True
            new_lines.append(line)

    if not found:
        new_lines.append('\nappx_package_name = "%s"\n' % appx_package_name)

    with open("setup.py", "w", encoding="utf-8") as f:
        f.writelines(new_lines)


def get_target_arch():
    archs = ensure_list(control.architecture)

    if "x64" in archs:
        return "x64"
    if "x86" in archs:
        return "x86"
    if "arm64" in archs:
        return "arm64"
    if "arm" in archs:
        return "arm"
    if "all" in archs:
        return "all"

    return os_arc()


def get_arch_score(file_arch, wanted_arch):
    if wanted_arch == "all":
        return 2
    if file_arch == wanted_arch:
        return 3
    if file_arch in ("all", "neutral"):
        return 2
    return 0


def get_type_score(filename):
    ext = os.path.splitext(filename.lower())[1]
    ext_priority = {
        ".msixbundle": 100,
        ".appxbundle": 90,
        ".msix": 80,
        ".appx": 70,
        ".emsixbundle": 60,
        ".eappxbundle": 50,
        ".emsix": 40,
        ".eappx": 30,
    }
    return ext_priority.get(ext, 0)


def update_package():
    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, proxies=proxies)
    package_json_fr = get_json_fr(store_id, proxies=proxies)

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

    ignored_versions = package_infos.get("ignored_versions", [])
    version_prefix = package_infos.get("version_prefix", None)
    package_name_filter = package_infos.get("package_name_filter", None)

    setup_appx_package_name = get_setup_appx_package_name()
    if not package_name_filter and setup_appx_package_name:
        package_name_filter = setup_appx_package_name

    if debug_mode:
        print("\n\n=== CAREFUL : DEBUG MODE IS ACTIVE ===\n")
        print(f"package_name_filter : {package_name_filter}")
        print(f"setup_appx_package_name : {setup_appx_package_name}")
        print(f"version_prefix : {version_prefix}")
        print(f"ignored_versions : {ignored_versions}\n")

    print("Retrieving informations from Microsoft Store API, please wait\n")
    all_files_dict = get_all_files(url_ms, proxies)

    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")

    bin_selected_dict = ask_app_filename(
        all_files_dict,
        package_name_filter=package_name_filter,
        ignored_versions=ignored_versions,
        version_prefix=version_prefix,
    )

    if not bin_selected_dict:
        error("No matching package found from API response")

    if debug_mode:
        bin_selected_dict_json = json.dumps(bin_selected_dict, indent=4)
        print("*** Selected file from ask_app_filename() ***")
        print(f"bin_selected_dict : {bin_selected_dict_json}\n")

    selected_package_name = bin_selected_dict[0]["bin_name"].split("_")[0]
    set_setup_appx_package_name(selected_package_name)

    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"]

    if bin_selected.get("is_encrypted"):
        print("Selected encrypted package: %s" % latest_bin)

    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:
        error("ERROR: The retrieved url will not download from microsoft's servers")

    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")
        ask_control_description("update_package", get_description(package_json))
        ask_control_description_fr("update_package", get_description_fr(package_json_fr))

    uwp_app_dict = make_dependency_dict(selected_package_name, all_files_dict)

    if not uwp_app_dict:
        error(f'"{selected_package_name}" not found in API file list. This is probably caused by package.json filters:\n{package_infos}')

    newer_uwp_app = next(iter(uwp_app_dict.values()))

    downloaded_dependencies = download_api_dependencies(bin_selected, all_files_dict, proxies)

    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)
        for dep in downloaded_dependencies:
            if isfile(dep["bin_name"]):
                remove_file(dep["bin_name"])
        return

    control.set_software_version(version)
    control.save_control_to_wapt()

    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 isfile(uwp_file) and not isfile(makepath(bins_dir, uwp_file)):
            shutil.move(uwp_file, bins_dir)

    for dep in downloaded_dependencies:
        dep_file = dep["bin_name"]
        if isfile(dep_file) and not isfile(makepath(bins_dir, dep_file)):
            shutil.move(dep_file, bins_dir)

    remove_outdated_binaries(
        version,
        ["appxbundle", "msixbundle", "appx", "msix", "eappx", "emsix", "eappxbundle", "emsixbundle", "part"],
        latest_bin,
    )

    return package_updated


def get_json(app_id, proxies=None):
    url = "https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/%s?market=US&locale=en-us&deviceFamily=Windows.Desktop" % app_id
    data = json.loads(http_get(url, proxies=proxies).text)
    return data


def get_json_fr(app_id, proxies=None):
    url = "https://storeedgefd.dsx.mp.microsoft.com/v9.0/products/%s?market=FR&locale=fr-fr&deviceFamily=Windows.Desktop" % app_id
    data_fr = json.loads(http_get(url, proxies=proxies).text)
    return data_fr


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

    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 = []

    encrypted_exts = {
        ".eappx",
        ".emsix",
        ".eappxbundle",
        ".emsixbundle",
    }

    for i, download_url in all_files.items():
        bin_name = i.replace("~", "_")
        version_match = re.search(r"_(\d+(?:\.\d+)+)", bin_name)
        version = version_match.group(1) if version_match else "0"

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

        lower_name = bin_name.lower()
        file_ext = os.path.splitext(lower_name)[1]
        is_encrypted = file_ext in encrypted_exts

        if "arm64" in lower_name:
            package_arch = "arm64"
        elif "arm" in lower_name:
            package_arch = "arm"
        elif "x64" in lower_name:
            package_arch = "x64"
        elif "x86" in lower_name:
            package_arch = "x86"
        elif "neutral" in lower_name:
            package_arch = "all"
        else:
            package_arch = "all"

        file_dict = {
            "version": version,
            "bin_name": bin_name,
            "package_name": package_name,
            "software_name": software_name,
            "package_arch": package_arch,
            "download_url": download_url,
            "file_ext": file_ext,
            "is_encrypted": is_encrypted,
            "modified": modified_dict.get(i),
            "main_file_name": main_file_name,
        }
        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"

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

        out = http_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 = http_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:
            for _ in range(4):
                out2 = http_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)

        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, AttributeError, IndexError):
                continue

        if not filenames:
            raise Exception("server returned an empty list")

        identities = {}
        modified_dict = {}
        for node in doc.getElementsByTagName("SecuredFragment"):
            try:
                file_name, modified = filenames[
                    node.parentNode.parentNode.parentNode.getElementsByTagName("ID")[0].firstChild.nodeValue
                ]
                update_identity = node.parentNode.parentNode.firstChild
                modified_dict[file_name] = modified
                identities[file_name] = (
                    update_identity.attributes["UpdateID"].value,
                    update_identity.attributes["RevisionNumber"].value,
                )
            except (KeyError, AttributeError, IndexError):
                continue

        api_dict = {value: identities[value] for value in modified_dict}

        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 = http_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_item = i.getElementsByTagName("Url")[0].firstChild.nodeValue
                if len(url_item) != 99:
                    file_dict[file_name] = url_item

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

        return file_dict, modified_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 = http_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_item, locale, installer_type, url_item = download_data[0]
        if len(download_data) > 1:
            for data_item in download_data[1:]:
                if arch_item not in ("neutral", curr_arch) and data_item[0] in ("neutral", curr_arch):
                    arch_item, locale, installer_type, url_item = data_item
                elif data_item[0] == arch_item and data_item[1] != locale and ("us" in data_item[1] or "en" in data_item[1]):
                    locale, installer_type, url_item = data_item[1], data_item[2], data_item[3]
                    break

        main_file_name = clean_name(file_name) + "." + installer_type
        file_dict = {main_file_name: url_item}
        return file_dict, {}, main_file_name

    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 = http_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, package_name_filter=None, ignored_versions=None, version_prefix=None):
    if ignored_versions is None:
        ignored_versions = []

    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"
        )
    ]

    candidates = []
    for app_file in selected:
        if package_name_filter:
            if not app_file["bin_name"].startswith(f"{package_name_filter}_"):
                continue

        if version_prefix is not None:
            n = len(version_prefix.split("."))
            if Version(app_file["version"], n) != Version(version_prefix, n):
                continue

        if any(
            Version(app_file["version"], len(v.split("."))) == Version(v, len(v.split(".")))
            for v in ignored_versions
        ):
            continue

        candidates.append(app_file)

    if not candidates:
        candidates = selected

    if not candidates:
        return []

    grouped = {}
    for item in candidates:
        pkg_name = item["bin_name"].split("_")[0]
        grouped.setdefault(pkg_name, []).append(item)

    target_arch = get_target_arch()

    if len(grouped) == 1:
        best = select_best_match(candidates, wanted_arch=target_arch)
        return [best] if best else []

    if "template-microsoft-store" in control.package:
        return waptguihelper.grid_dialog(
            "Please select the proper file",
            json.dumps(candidates),
            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}]}',
        )

    best = select_best_match(candidates, wanted_arch=target_arch)
    return [best] if best else []


def ask_control_categories():
    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):
    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):
    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()

    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):
    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=""):
    control.description = ask_dialog("Description (en)", "Please fill the description (english)", description_from_store)
    control.save_control_to_wapt()
    return control.description


def ask_control_description_fr(blank_str=None, description_from_store=""):
    control.description_fr = ask_dialog("Description (fr)", "Merci de remplir la description (français)", description_from_store)
    control.description_pl = ""
    control.description_de = ""
    control.description_es = ""
    control.description_pt = ""
    control.description_it = ""
    control.description_nl = ""
    control.description_ru = ""
    control.save_control_to_wapt()
    return control.description_fr


def ask_dialog(title, text, default="", stay_on_top=False):
    return waptguihelper.input_dialog(title, text, default, stay_on_top)


def get_description(data):
    return data["Payload"]["Skus"][0]["Description"].replace("\r\n", "").replace("\n", "").replace("\r", "")


def get_description_fr(data_fr):
    return data_fr["Payload"]["Skus"][0]["Description"].replace("\r\n", "").replace("\n", "").replace("\r", "")


def get_package_ext_priority(filename):
    ext_priority = {
        ".msixbundle": 100,
        ".appxbundle": 90,
        ".msix": 80,
        ".appx": 70,
        ".emsixbundle": 60,
        ".eappxbundle": 50,
        ".emsix": 40,
        ".eappx": 30,
    }
    return ext_priority.get(os.path.splitext(filename.lower())[1], 0)


def select_best_match(files_list, wanted_arch=None):
    if not files_list:
        return None

    if wanted_arch is None:
        wanted_arch = get_target_arch()

    compatible = []
    for item in files_list:
        if wanted_arch == "all" or item["package_arch"] in (wanted_arch, "all"):
            compatible.append(item)

    if not compatible:
        compatible = files_list

    def score(x):
        return (
            get_arch_score(x["package_arch"], wanted_arch),
            get_type_score(x["bin_name"]),
            parse_iso_datetime(x.get("modified")),
            Version(x["version"]),
        )

    return max(compatible, key=score)


def is_framework_package(item):
    framework_prefixes = (
        "Microsoft.VCLibs",
        "Microsoft.UI.Xaml",
        "Microsoft.NET.Native.Framework",
        "Microsoft.NET.Native.Runtime",
        "Microsoft.Services.Store.Engagement",
        "Microsoft.WindowsAppRuntime",
    )
    return item["bin_name"].startswith(framework_prefixes)


def same_or_compatible_arch(main_arch, dep_arch):
    if main_arch == "all":
        return True
    if dep_arch == "all":
        return True
    return dep_arch == main_arch


def make_dependency_dict(selected_package_name, all_files_dict):
    candidates = [
        f for f in all_files_dict
        if f["bin_name"].startswith(f"{selected_package_name}_")
    ]

    best = select_best_match(candidates, wanted_arch=get_target_arch())
    if not best:
        return {}

    return {
        best["bin_name"]: {
            "FileName": best["bin_name"],
            "Name": best["bin_name"].split("_")[0],
            "Version": best["version"],
            "Architecture": best["package_arch"],
            "Dependencies": None,
        }
    }


def get_newer_uwp_depency(package_name, all_files_dict, package_arch=None, version_prefix=None, min_version=None):
    candidates = []

    for uwp_app in all_files_dict:
        if not uwp_app["bin_name"].startswith(f"{package_name}_"):
            continue

        if package_arch is not None and not same_or_compatible_arch(package_arch, uwp_app["package_arch"]):
            continue

        if version_prefix is not None:
            n = len(version_prefix.split("."))
            if Version(uwp_app["version"], n) != Version(version_prefix, n):
                continue

        if min_version is not None and Version(uwp_app["version"]) < Version(min_version):
            continue

        candidates.append(uwp_app)

    return select_best_match(candidates, wanted_arch=package_arch or get_target_arch())


def resolve_framework_dependencies(main_package, all_files_dict):
    main_arch = main_package["package_arch"]

    framework_groups = {
        "Microsoft.VCLibs": [],
        "Microsoft.UI.Xaml": [],
        "Microsoft.NET.Native.Framework": [],
        "Microsoft.NET.Native.Runtime": [],
        "Microsoft.Services.Store.Engagement": [],
        "Microsoft.WindowsAppRuntime": [],
    }

    for item in all_files_dict:
        if not is_framework_package(item):
            continue

        if not same_or_compatible_arch(main_arch, item["package_arch"]):
            continue

        for prefix in framework_groups:
            if item["bin_name"].startswith(prefix + "_"):
                framework_groups[prefix].append(item)
                break

    resolved = []
    for _, matches in framework_groups.items():
        best = select_best_match(matches, wanted_arch=main_arch)
        if best:
            resolved.append(best)

    return resolved


def resolve_all_dependencies(main_package, all_files_dict):
    main_name = main_package["bin_name"].split("_")[0]
    main_arch = main_package["package_arch"]

    grouped = {}
    for item in all_files_dict:
        pkg_name = item["bin_name"].split("_")[0]

        if pkg_name == main_name:
            continue

        if not same_or_compatible_arch(main_arch, item["package_arch"]):
            continue

        grouped.setdefault(pkg_name, []).append(item)

    resolved = []
    for _, matches in grouped.items():
        best = select_best_match(matches, wanted_arch=main_arch)
        if best:
            resolved.append(best)

    return resolved


def resolve_dependencies(main_package, all_files_dict, all_dependencies=False):
    if all_dependencies:
        return resolve_all_dependencies(main_package, all_files_dict)
    return resolve_framework_dependencies(main_package, all_files_dict)


def download_api_dependencies(main_package, all_files_dict, proxies):
    downloaded_dependencies = []
    dependencies = resolve_dependencies(
        main_package,
        all_files_dict,
        all_dependencies=download_all_dependencies,
    )

    if dependencies and debug_mode:
        print("\nResolved API dependencies:")
        for dep in dependencies:
            print(f' - {dep["bin_name"]}')
        print("")

    for dep in dependencies:
        latest_bin = dep["bin_name"]
        download_url = dep["download_url"]

        if download_url.split("/")[2].endswith("microsoft.com"):
            if not isfile(latest_bin):
                print("Downloading dependency: %s" % latest_bin)
                wget(download_url, latest_bin, proxies=proxies)
            else:
                print("Dependency already present: %s" % latest_bin)
            downloaded_dependencies.append(dep)
        else:
            error("ERROR: The retrieved URL will not download from microsoft's servers")

    return downloaded_dependencies


def get_uwp_filename_arch(appx_filename, appx_package_name=None):
    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 appx_package_name not 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"

b3100f592fa1a3b867363176529f2df82d51bbcc9221dfc95a54f1392a58cf9d : WAPT/README.md
deb8ca89091812a8663d6e901430325f9de1d067e1e1d4f92ac0522e585f9ce9 : WAPT/README_fr.md
38d056ab130f7bf7c481c12636a4e9959de36561d3dfcbe54c6e3571bc0c1dc3 : WAPT/certificate.crt
3a6df07709510e8084cd91c49cc9f6b9ea7a96856e3625465fe4a64da84f5814 : WAPT/changelog.txt
19b70467c3a2cecc663a991738a1cb4354d6b4a68a346a4fb0c26bc398f24c9b : WAPT/control
aea6d0c53867b3d774e670da830d8ef922bd93a4ca37ea565e7bbb8465152983 : WAPT/icon.png
87370426807b83b7916c4f43942cd9bf71b1eeaf3b44728d241efe107927e3a2 : data/xml/FE3FileUrl.xml
2d3dd983c9d83c2464afaa85ab49ad5c78a537a131ec61e0c32f6b446bed4f55 : data/xml/GetCookie.xml
f8a4681fbeafb4ddcaac37b406374143900d8885b59ba7a7a0ec782d79bacd9b : data/xml/WUIDRequest.xml
14d227a2bfacb5437e7304c4522903b46de4e21687c7105dc633c4a5f36c92f5 : luti.json
bd43e5749147a75dbd658cac066506657073da0ee363e93418ed66427dae1bb6 : package.json
b012c7506e1aae774e030f73a8b09bf3a8798b33b1307a51c2464c843f73197b : setup.py
c802d056a635d7c09e516c827ce4f49551b98844183adf0aa5896ea5bdfd2a03 : setupdevhelpers.py
430b03a6bdd562e9c5ff06ba4aec2ac054cf2c9e01ed3cb6e3575bd675d5d1fd : 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