From 71fd3115e20987b361ec7b1eaa81cee1b87ab332 Mon Sep 17 00:00:00 2001 From: foxster-mp4 Date: Wed, 16 Aug 2023 20:32:11 -0700 Subject: [PATCH] Support AltStore's modern app permissions --- app.html | 26 ++++-- css/app.css | 57 ++++++++++++ css/style.css | 14 ++- js/app.js | 136 +++++++++++------------------ js/components/AppPermissionItem.js | 15 ++++ js/constants.js | 119 ++++++++++++++++++++++++- js/utilities.js | 9 ++ 7 files changed, 281 insertions(+), 95 deletions(-) create mode 100644 js/components/AppPermissionItem.js diff --git a/app.html b/app.html index 06ceea0..e2fcc76 100644 --- a/app.html +++ b/app.html @@ -97,13 +97,27 @@
-

Permissions

+

App Permissions

-
- -
-

None

-

The developer has not specified any permissions required by this app.

+
+
+
+ +

None

+

This app's permissions have not been specified by the developer.

+
+
+
+
+
+
+ +

Entitlements

+

Entitlements are additional permissions that grant access to certain system services, + including potentially sensitive information.

+
+
+
diff --git a/css/app.css b/css/app.css index 2761ec9..464c83c 100644 --- a/css/app.css +++ b/css/app.css @@ -172,6 +172,63 @@ a { opacity: 0.5; } +#permission-containers { + display: flex; + flex-direction: column; + gap: 1.75em; + margin-top: 1em; + margin-bottom: 0.5em; +} + +.permission-icon { + color: var(--app-tint-color) !important; + font-size: 2em; +} + +.permission-container { + border-radius: 1.5em; + padding: 1.25em; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15); +} + +.permission-container-header { + text-align: center; + margin-bottom: 0.5em; +} + +.permission-container p.description { + opacity: 0.5; + line-height: 1.5em; +} + +.permission-container p { + font-size: 0.9em; +} + +.permission-container b { + font-size: 1.25em; +} + +.permission-items { + margin: 0.5em 0; + display: grid; + grid-template-columns: 49.25% 49.25%; + row-gap: 4px; +} + +.permission-item { + display: flex; + align-items: center; + gap: 8px; + width: fit-content; + font-size: 0.85em; + color: black; +} + +.permission-item i { + font-size: 1.5em !important; +} + /* Source */ #source { padding-top: 0; diff --git a/css/style.css b/css/style.css index 9b3b882..b5e886f 100644 --- a/css/style.css +++ b/css/style.css @@ -12,12 +12,14 @@ --accent-color: #018084; --app-tint-color: #018084; - --color-bg: rgba(255, 255, 255, 255); + --color-bg: rgba(255, 255, 255, 1); + --color-bg-secondary: rgba(235, 235, 235, 1); --color-separator: rgba(0, 0, 0, 0.15); - --color-primary: rgba(0, 122, 254, 255); + --color-primary: rgba(0, 122, 254, 1); --color-transparent: rgba(255, 255, 255, 0.75); --color-bg-dark: rgb(26, 25, 27); + --color-bg-dark-secondary: rgb(46, 45, 47); --color-separator-dark: rgba(255, 255, 255, 0.15); --color-primary-dark: rgba(11, 132, 254, 255); --color-transparent-dark: rgba(26, 25, 27, 0.25); @@ -64,6 +66,14 @@ .screenshot { border-color: rgba(255, 255, 255, 0.2) !important; } + + .secondary-bg { + background-color: var(--color-bg-dark-secondary) !important; + } +} + +.secondary-bg { + background-color: white; } body.loading { diff --git a/js/app.js b/js/app.js index ae7be43..d6b9ad7 100644 --- a/js/app.js +++ b/js/app.js @@ -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", ` -
- -
-

${permissionType}

-

${permission.usageDescription ?? "No description provided."}

-
-
`); + // + // 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"); diff --git a/js/components/AppPermissionItem.js b/js/components/AppPermissionItem.js new file mode 100644 index 0000000..ce2ceba --- /dev/null +++ b/js/components/AppPermissionItem.js @@ -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) => ` + +

+

${name}

+
+`; \ No newline at end of file diff --git a/js/constants.js b/js/constants.js index 7ebebcf..5f1b7f8 100644 --- a/js/constants.js +++ b/js/constants.js @@ -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; \ No newline at end of file +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" + } +} \ No newline at end of file diff --git a/js/utilities.js b/js/utilities.js index 586b592..c4c0458 100644 --- a/js/utilities.js +++ b/js/utilities.js @@ -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)); }