Support AltStore's modern app permissions

This commit is contained in:
foxster-mp4
2023-08-16 20:32:11 -07:00
parent 4c0a10419b
commit 71fd3115e2
7 changed files with 281 additions and 95 deletions

136
js/app.js
View File

@@ -6,9 +6,11 @@
// MIT License.
//
import { urlSearchParams, sourceURL } from "./constants.js";
import { formatString } from "./utilities.js";
import { urlSearchParams, sourceURL, legacyPermissions } from "./constants.js";
import { formatString, insertSpaceInCamelString, insertSpaceInSnakeString } from "./utilities.js";
import { main } from "./main.js";
import { privacy, entitlements } from "./constants.js";
import { AppPermissionItem } from "./components/AppPermissionItem.js";
if (!urlSearchParams.has('id')) exit();
const bundleId = urlSearchParams.get('id');
@@ -160,95 +162,57 @@ main((json) => {
//
// Permissions
const permissions = document.getElementById("permissions");
// If permissions specified
if (app.permissions) {
// Remove placeholder permission
permissions.querySelector(".permission").remove();
//
// Privacy
const privacyContainer = document.getElementById("privacy");
if (app.appPermissions?.privacy?.length || app.permissions) {
privacyContainer.querySelector(".permission-icon").classList = "permission-icon bi-person-fill-lock";
privacyContainer.querySelector("b").innerText = "Privacy";
privacyContainer.querySelector(".description").innerText = `"${app.name}" may request to access the following:`;
}
app.appPermissions?.privacy?.forEach(privacyPermission => {
const permission = privacy[privacyPermission.name];
let name = permission?.name ?? insertSpaceInCamelString(privacyPermission.name),
icon;
if (permission?.icon) icon = permission.icon;
else icon = "gear-wide-connected";
privacyContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
AppPermissionItem(name, icon, privacyPermission?.usageDescription)
);
});
app.permissions?.forEach(permission => {
var permissionType, icon;
switch (permission.type) {
// AltStore-supported permissions
case "background-audio":
permissionType = "Background Audio";
icon = "volume-up-fill";
break;
case "background-fetch":
permissionType = "Background Fetch";
icon = "arrow-repeat"
break;
case "photos":
permissionType = "Photos"
icon = "image-fill";
break;
// Additional permissions
case "camera":
permissionType = "Camera"
icon = "camera-fill";
break;
case "music":
permissionType = "Music Library"
icon = "music-note-list";
break;
case "location":
permissionType = "Location"
icon = "geo-alt-fill";
break;
case "microphone":
permissionType = "Microphone"
icon = "mic-fill";
break;
case "contacts":
permissionType = "Contacts"
icon = "people-fill";
break;
case "bluetooth":
permissionType = "Bluetooth"
icon = "bluetooth";
break;
case "faceid":
permissionType = "Face ID"
icon = "person-bounding-box";
break;
case "network":
permissionType = "Network"
icon = "wifi";
break;
case "calendar":
case "calendars":
permissionType = "Calendar"
icon = "calendar-date";
break;
case "reminders":
permissionType = "Reminders"
icon = "list-ul";
break;
case "siri":
permissionType = "Siri"
icon = "gear-wide-connected";
break;
case "speech-recognition":
permissionType = "Speech Recognition"
icon = "soundwave";
break;
default:
permissionType = permission.type.replaceAll("-", " ");
icon = "gear-wide-connected";
break;
}
permissions.insertAdjacentHTML("beforeend", `
<div class="permission">
<i class="bi-${icon}" style="color: ${tintColor};"></i>
<div class="text">
<p class="title">${permissionType}</p>
<p class="description">${permission.usageDescription ?? "No description provided."}</p>
</div>
</div>`);
//
// Legacy permissions
if (!app.appPermissions?.privacy) {
app.permissions?.forEach(appPermission => {
const permission = legacyPermissions[appPermission.type];
let name = insertSpaceInSnakeString(appPermission.type),
icon;
if (permission?.icon) icon = permission.icon;
else icon = "gear-wide-connected";
privacyContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
AppPermissionItem(name, icon, appPermission?.usageDescription)
);
});
}
//
// Entitlements
const entitlementsContainer = document.getElementById("entitlements");
console.log(app.appPermissions?.entitlements);
if (!app.appPermissions?.entitlements?.length) entitlementsContainer.remove();
app.appPermissions?.entitlements.forEach(entitlementPermission => {
const permission = entitlements[entitlementPermission.name];
let name = permission?.name ?? insertSpaceInSnakeString(entitlementPermission.name),
icon;
if (permission?.icon) icon = permission.icon;
else icon = "gear-wide-connected";;
entitlementsContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
AppPermissionItem(name, icon, permission?.description)
);
});
//
// Source info
const source = document.getElementById("source");

View File

@@ -0,0 +1,15 @@
// AppPermissionItem.js
// altsource-viewer (https://github.com/therealFoxster/altsource-viewer)
//
// Copyright (c) 2023 Foxster.
// MIT License.
//
export const AppPermissionItem = (name, icon, details) => `
<a class="permission-item"
onclick="alert('${details?.replace(/(['"])/g, "\\$1") ?? "altsource-viewer does not have detailed information about this entitlement."}');"
>
<p><i class="bi-${icon}"></i></p>
<p class="title">${name}</p>
</a>
`;

View File

@@ -7,6 +7,123 @@
//
export const urlSearchParams = new URLSearchParams(window.location.search);
export const sourceURL = urlSearchParams.get('source')?.replaceAll("+", "%2B");
// https://stackoverflow.com/a/8943487
export const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
export const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
export const entitlements = {
"com.apple.security.application-groups": {
name: "App Groups",
description: "Allow app to share files with other apps and app extensions in the same App Group.",
icon: "columns-gap"
},
"com.apple.developer.associated-domains": {
name: "Associated Domains",
description: "The associated domains for specific services, such as shared web credentials, universal links, and App Clips.",
icon: "globe2"
},
"com.apple.developer.carplay-audio": {
name: "CarPlay Audio",
description: "Allows the app the provide audio content for CarPlay.",
icon: "car-front-fill"
},
"get-task-allow": {
name: "Debuggable",
description: "Allow developers to attach a debugger to this app. This permission is required for JIT to work.",
icon: "tools"
},
"com.apple.developer.device-information.user-assigned-device-name": {
name: "Device Name",
description: "Grants access to the user-assigned device name instead of a generic device name.",
icon: "phone-fill"
},
"keychain-access-groups": {
name: "Keychain",
description: "Allows app to read and write secure data to the system's keychain.",
icon: "key-fill"
},
"com.apple.developer.networking.multicast": {
name: "Multicast",
description: "App can send or receive IP multicast traffic.",
icon: "globe2"
},
"aps-environment": {
name: "Push Notifications",
description: "App can send push notifications.",
icon: "app-indicator"
},
"com.apple.developer.applesignin": {
name: "Sign in with Apple",
description: "Allows sign in with Apple.",
icon: "apple"
},
"com.apple.developer.siri": {
name: "Siri",
description: "Allows app to handle Siri requests.",
icon: "mic-fill"
},
"com.apple.developer.networking.wifi-info": {
name: "Wi-Fi Information Access",
description: "Allows app to access information about the connected Wi-Fi network.",
icon: "wifi"
}
};
export const privacy = {
"AppleMusic": {
icon: "music-note-beamed"
},
"BluetoothAlways": {
name: "Bluetooth",
icon: "bluetooth"
},
"BluetoothPeripheral": {
name: "Bluetooth (Peripherals)",
icon: "bluetooth"
},
"Contacts": {
icon: "person-circle"
},
"Camera": {
icon: "camera-fill"
},
"FaceID": {
name: "Face ID",
icon: "person-bounding-box"
},
"LocalNetwork": {
icon: "globe2"
},
"LocationWhenInUse": {
name: "Location (When Using)",
icon: "cursor-fill"
},
"Microphone": {
icon: "mic-fill"
},
"PhotoLibrary": {
name: "Photos",
icon: "images"
},
"PhotoLibraryAdd": {
name: "Photos (Add)",
icon: "image"
},
"UserTracking": {
icon: "person-vcard-fill"
},
}
export const legacyPermissions = {
"background-audio": {
icon: "volume-up-fill"
},
"background-fetch": {
icon: "arrow-repeat"
},
"photos": {
icon: "images"
}
}

View File

@@ -10,6 +10,15 @@ import { AltStoreBanner } from "./components/AltStoreBanner.js";
import { NavigationBar } from "./components/NavigationBar.js";
import { urlRegex } from "./constants.js";
export function insertSpaceInSnakeString(string) {
return string.split(".").slice(-1)[0].split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
}
export function insertSpaceInCamelString(string) {
// https://stackoverflow.com/a/38388188/19227228
return string.match(/[A-Z][a-z]+|[0-9]+/g).join(" ");
}
export function insertAltStoreBanner(sourceName) {
document.getElementById("top")?.insertAdjacentHTML("afterbegin", AltStoreBanner(sourceName));
}