From 3aafa515034bc36d21a8b45772f1abc4c2f7bbbe Mon Sep 17 00:00:00 2001 From: opa334 Date: Sat, 21 Jan 2023 13:52:39 +0100 Subject: [PATCH] 1.5.0~b1 --- RootHelper/main.m | 470 +++++++++++------- Shared/TSListControllerShared.h | 1 + Shared/TSListControllerShared.m | 12 +- Shared/TSUtil.h | 1 + Shared/TSUtil.m | 15 +- TrollStore/TSApplicationsManager.m | 55 +- TrollStore/TSInstallationController.h | 2 + TrollStore/TSInstallationController.m | 43 ++ TrollStore/TSSceneDelegate.m | 18 + TrollStore/TSSettingsAdvancedListController.h | 5 + TrollStore/TSSettingsAdvancedListController.m | 103 ++++ TrollStore/TSSettingsListController.h | 1 + TrollStore/TSSettingsListController.m | 125 +++-- cert.p12 | Bin 4901 -> 4870 bytes cert_new.p12 | Bin 4870 -> 0 bytes 15 files changed, 616 insertions(+), 235 deletions(-) create mode 100644 TrollStore/TSSettingsAdvancedListController.h create mode 100644 TrollStore/TSSettingsAdvancedListController.m delete mode 100644 cert_new.p12 diff --git a/RootHelper/main.m b/RootHelper/main.m index 2fe6922..c83536d 100644 --- a/RootHelper/main.m +++ b/RootHelper/main.m @@ -123,6 +123,33 @@ NSString* appPathForAppId(NSString* appId) return nil; } +NSString* findAppNameInBundlePath(NSString* bundlePath) +{ + NSArray* bundleItems = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bundlePath error:nil]; + for(NSString* bundleItem in bundleItems) + { + if([bundleItem.pathExtension isEqualToString:@"app"]) + { + return bundleItem; + } + } + return nil; +} + +NSString* findAppPathInBundlePath(NSString* bundlePath) +{ + NSString* appName = findAppNameInBundlePath(bundlePath); + if(!appName) return nil; + return [bundlePath stringByAppendingPathComponent:appName]; +} + +NSURL* findAppURLInBundleURL(NSURL* bundleURL) +{ + NSString* appName = findAppNameInBundlePath(bundleURL.path); + if(!appName) return nil; + return [bundleURL URLByAppendingPathComponent:appName]; +} + BOOL isMachoFile(NSString* filePath) { FILE* file = fopen(filePath.fileSystemRepresentation, "r"); @@ -205,12 +232,13 @@ void setTSURLSchemeState(BOOL newState, NSString* customAppPath) } } - -void installLdid(NSString* ldidToCopyPath) +void installLdid(NSString* ldidToCopyPath, NSString* ldidVersion) { if(![[NSFileManager defaultManager] fileExistsAtPath:ldidToCopyPath]) return; NSString* ldidPath = [trollStoreAppPath() stringByAppendingPathComponent:@"ldid"]; + NSString* ldidVersionPath = [trollStoreAppPath() stringByAppendingPathComponent:@"ldid.version"]; + if([[NSFileManager defaultManager] fileExistsAtPath:ldidPath]) { [[NSFileManager defaultManager] removeItemAtPath:ldidPath error:nil]; @@ -218,8 +246,11 @@ void installLdid(NSString* ldidToCopyPath) [[NSFileManager defaultManager] copyItemAtPath:ldidToCopyPath toPath:ldidPath error:nil]; + NSData* ldidVersionData = [ldidVersion dataUsingEncoding:NSUTF8StringEncoding]; + [ldidVersionData writeToFile:ldidVersionPath atomically:YES]; + chmod(ldidPath.fileSystemRepresentation, 0755); - chown(ldidPath.fileSystemRepresentation, 0, 0); + chmod(ldidVersionPath.fileSystemRepresentation, 0644); } BOOL isLdidInstalled(void) @@ -433,7 +464,6 @@ int signApp(NSString* appPath) NSObject *tsBundleIsPreSigned = appInfoDict[@"TSBundlePreSigned"]; if([tsBundleIsPreSigned isKindOfClass:[NSNumber class]]) { - // if TSBundlePreSigned = YES, this bundle has been externally signed so we can skip over signing it now NSNumber *tsBundleIsPreSignedNum = (NSNumber *)tsBundleIsPreSigned; if([tsBundleIsPreSignedNum boolValue] == YES) @@ -442,7 +472,7 @@ int signApp(NSString* appPath) return 0; } } - + SecStaticCodeRef codeRef = getStaticCodeRef(executablePath); if(codeRef != NULL) { @@ -478,34 +508,8 @@ int signApp(NSString* appPath) } else { - // Work around an ldid bug where it doesn't keep entitlements on stray binaries - NSMutableDictionary* storedEntitlements = [NSMutableDictionary new]; - NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[NSURL fileURLWithPath:appPath] includingPropertiesForKeys:nil options:0 errorHandler:nil]; - NSURL* fileURL; - while(fileURL = [enumerator nextObject]) - { - NSString* filePath = fileURL.path; - if(isMachoFile(filePath)) - { - storedEntitlements[filePath] = dumpEntitlementsFromBinaryAtPath(filePath); - } - } - // app has entitlements, keep them ldidRet = runLdid(@[@"-s", certArg, appPath], nil, &errorOutput); - - [storedEntitlements enumerateKeysAndObjectsUsingBlock:^(NSString* binaryPath, NSDictionary* entitlements, BOOL* stop) - { - NSDictionary* newEntitlements = dumpEntitlementsFromBinaryAtPath(binaryPath); - if(!newEntitlements || ![newEntitlements isEqualToDictionary:entitlements]) - { - NSString* tmpEntitlementPlistPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ent.xml"]; - [entitlements writeToURL:[NSURL fileURLWithPath:tmpEntitlementPlistPath] error:nil]; - NSString* tmpEntitlementArg = [@"-S" stringByAppendingString:tmpEntitlementPlistPath]; - runLdid(@[tmpEntitlementArg, certArg, binaryPath], nil, nil); - [[NSFileManager defaultManager] removeItemAtPath:tmpEntitlementPlistPath error:nil]; - } - }]; } NSLog(@"ldid exited with status %d", ldidRet); @@ -581,7 +585,7 @@ void applyPatchesToInfoDictionary(NSString* appPath) // 172: no info.plist found in app // 173: app is not signed and cannot be signed because ldid not installed or didn't work // 174: -int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate) +int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate, BOOL useInstalldMethod) { NSLog(@"[installApp force = %d]", force); @@ -590,18 +594,18 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate) NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appPayloadPath error:nil]; if(!items) return 167; - NSString* appBundlePath; + NSString* appBundleToInstallPath; for(NSString* item in items) { if([item.pathExtension isEqualToString:@"app"]) { - appBundlePath = [appPayloadPath stringByAppendingPathComponent:item]; + appBundleToInstallPath = [appPayloadPath stringByAppendingPathComponent:item]; break; } } - if(!appBundlePath) return 167; + if(!appBundleToInstallPath) return 167; - NSString* appId = appIdForAppPath(appBundlePath); + NSString* appId = appIdForAppPath(appBundleToInstallPath); if(!appId) return 176; if(([appId.lowercaseString isEqualToString:@"com.opa334.trollstore"] && !isTSUpdate) || [immutableAppBundleIdentifiers() containsObject:appId.lowercaseString]) @@ -609,115 +613,151 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate) return 179; } - if(!infoDictionaryForAppPath(appBundlePath)) return 172; + if(!infoDictionaryForAppPath(appBundleToInstallPath)) return 172; if(!isTSUpdate) { - applyPatchesToInfoDictionary(appBundlePath); + applyPatchesToInfoDictionary(appBundleToInstallPath); } if(sign) { - int signRet = signApp(appBundlePath); + int signRet = signApp(appBundleToInstallPath); if(signRet != 0) return signRet; } - loadMCMFramework(); + MCMAppContainer* appContainer = [objc_getClass("MCMAppContainer") containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil]; + if(appContainer) + { + // App update + // Replace existing bundle with new version - BOOL existed; - NSError* mcmError; - MCMAppContainer* appContainer = [objc_getClass("MCMAppContainer") containerWithIdentifier:appId createIfNecessary:YES existed:&existed error:&mcmError]; - if(!appContainer || mcmError) - { - NSLog(@"[installApp] failed to create app container for %@: %@", appId, mcmError); - return 170; - } + // Check if the existing app bundle is empty + NSURL* bundleContainerURL = appContainer.url; + NSURL* appBundleURL = findAppURLInBundleURL(bundleContainerURL); - if(existed) - { - NSLog(@"[installApp] got existing app container: %@", appContainer); - } - else - { - NSLog(@"[installApp] created app container: %@", appContainer); - } - - // check if the bundle is empty - BOOL isEmpty = YES; - NSArray* bundleItems = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appContainer.url.path error:nil]; - for(NSString* bundleItem in bundleItems) - { - if([bundleItem.pathExtension isEqualToString:@"app"]) + // Make sure the installed app is a TrollStore app or the container is empty (or the force flag is set) + NSURL* trollStoreMarkURL = [bundleContainerURL URLByAppendingPathComponent:@"_TrollStore"]; + if(!appBundleURL && ![trollStoreMarkURL checkResourceIsReachableAndReturnError:nil] && !force) { - isEmpty = NO; - break; + NSLog(@"[installApp] already installed and not a TrollStore app... bailing out"); + return 171; } - } - NSLog(@"[installApp] container is empty? %d", isEmpty); - - // Make sure there isn't already an app store app installed with the same identifier - NSURL* trollStoreMarkURL = [appContainer.url URLByAppendingPathComponent:@"_TrollStore"]; - if(existed && !isEmpty && ![trollStoreMarkURL checkResourceIsReachableAndReturnError:nil] && !force) - { - NSLog(@"[installApp] already installed and not a TrollStore app... bailing out"); - return 171; - } - - // Mark app as TrollStore app - BOOL marked = [[NSFileManager defaultManager] createFileAtPath:trollStoreMarkURL.path contents:[NSData data] attributes:nil]; - if(!marked) - { - NSLog(@"[installApp] failed to mark %@ as TrollStore app", appId); - return 177; - } - - fixPermissionsOfAppBundle(appBundlePath); - - // Wipe old version if needed - if(existed) - { - if(![appId isEqualToString:@"com.opa334.TrollStore"]) + // Terminate app if it's still running + if(!isTSUpdate) { BKSTerminateApplicationForReasonAndReportWithDescription(appId, 5, false, @"TrollStore - App updated"); } - NSLog(@"[installApp] found existing TrollStore app, cleaning directory"); - NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:appContainer.url includingPropertiesForKeys:nil options:0 errorHandler:nil]; - NSURL* fileURL; - while(fileURL = [enumerator nextObject]) + NSLog(@"[installApp] replacing existing app with new version"); + + // Delete existing .app directory if it exists + if(appBundleURL) { - // do not under any circumstance delete this file as it makes iOS loose the app registration - if([fileURL.lastPathComponent isEqualToString:@".com.apple.mobile_container_manager.metadata.plist"] || [fileURL.lastPathComponent isEqualToString:@"_TrollStore"]) - { - NSLog(@"[installApp] skipping removal of %@", fileURL); - continue; - } - - [[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil]; + [[NSFileManager defaultManager] removeItemAtURL:appBundleURL error:nil]; } - } - // Install app - NSString* newAppBundlePath = [appContainer.url.path stringByAppendingPathComponent:appBundlePath.lastPathComponent]; - NSLog(@"[installApp] new app path: %@", newAppBundlePath); - - NSError* copyError; - BOOL suc = [[NSFileManager defaultManager] copyItemAtPath:appBundlePath toPath:newAppBundlePath error:©Error]; - if(suc) - { - NSLog(@"[installApp] App %@ installed, adding to icon cache now...", appId); - registerPath(newAppBundlePath, NO, YES); - return 0; + NSString* newAppBundlePath = [bundleContainerURL.path stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent]; + NSLog(@"[installApp] new app path: %@", newAppBundlePath); + + // Install new version into existing app bundle + NSError* copyError; + BOOL suc = [[NSFileManager defaultManager] copyItemAtPath:appBundleToInstallPath toPath:newAppBundlePath error:©Error]; + if(!suc) + { + NSLog(@"[installApp] Error copying new version during update: %@", copyError); + return 178; + } } else { - NSLog(@"[installApp] Failed to copy app bundle for app %@, error: %@", appId, copyError); - return 178; + // Initial app install + BOOL systemMethodSuccessful = NO; + if(useInstalldMethod) + { + // System method + // Do initial placeholder installation using LSApplicationWorkspace + NSLog(@"[installApp] doing placeholder installation using LSApplicationWorkspace"); + + NSError* installError; + @try + { + systemMethodSuccessful = [[LSApplicationWorkspace defaultWorkspace] installApplication:[NSURL fileURLWithPath:appPackagePath] withOptions:@{ + LSInstallTypeKey : @1, + @"PackageType" : @"Placeholder" + } error:&installError]; + } + @catch(NSException* e) + { + NSLog(@"[installApp] encountered expection %@ while trying to do placeholder install", e); + systemMethodSuccessful = NO; + } + + if(!systemMethodSuccessful) + { + NSLog(@"[installApp] encountered error %@ while trying to do placeholder install", installError); + } + } + + if(!systemMethodSuccessful) + { + // Custom method + // Manually create app bundle via MCM apis and move app there + NSLog(@"[installApp] doing custom installation using MCMAppContainer"); + + NSError* mcmError; + appContainer = [objc_getClass("MCMAppContainer") containerWithIdentifier:appId createIfNecessary:YES existed:nil error:&mcmError]; + + if(!appContainer || mcmError) + { + NSLog(@"[installApp] failed to create app container for %@: %@", appId, mcmError); + return 170; + } + else + { + NSLog(@"[installApp] created app container: %@", appContainer); + } + + NSString* newAppBundlePath = [appContainer.url.path stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent]; + NSLog(@"[installApp] new app path: %@", newAppBundlePath); + + NSError* copyError; + BOOL suc = [[NSFileManager defaultManager] copyItemAtPath:appBundleToInstallPath toPath:newAppBundlePath error:©Error]; + if(!suc) + { + + NSLog(@"[installApp] Failed to copy app bundle for app %@, error: %@", appId, copyError); + return 178; + } + } } + + appContainer = [objc_getClass("MCMAppContainer") containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil]; + + // Mark app as TrollStore app + NSURL* trollStoreMarkURL = [appContainer.url URLByAppendingPathComponent:@"_TrollStore"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:trollStoreMarkURL.path]) + { + NSError* creationError; + NSData* emptyData = [NSData data]; + BOOL marked = [emptyData writeToURL:trollStoreMarkURL options:0 error:&creationError]; + if(!marked) + { + NSLog(@"[installApp] failed to mark %@ as TrollStore app by creating %@, error: %@", appId, trollStoreMarkURL.path, creationError); + return 177; + } + } + + // At this point the (new version of the) app is installed but still needs to be registered + // Also permissions need to be fixed + NSURL* updatedAppURL = findAppURLInBundleURL(appContainer.url); + fixPermissionsOfAppBundle(updatedAppURL.path); + registerPath(updatedAppURL.path, 0, YES); + return 0; } -int uninstallApp(NSString* appPath, NSString* appId) +int uninstallApp(NSString* appPath, NSString* appId, BOOL useCustomMethod) { BOOL deleteSuc = NO; if(!appId && appPath) @@ -760,7 +800,21 @@ int uninstallApp(NSString* appPath, NSString* appId) } } - deleteSuc = [[LSApplicationWorkspace defaultWorkspace] uninstallApplication:appId withOptions:nil]; + BOOL systemMethodSuccessful = NO; + if(!useCustomMethod) + { + systemMethodSuccessful = [[LSApplicationWorkspace defaultWorkspace] uninstallApplication:appId withOptions:nil]; + } + + if(!systemMethodSuccessful) + { + deleteSuc = [[NSFileManager defaultManager] removeItemAtPath:[appPath stringByDeletingLastPathComponent] error:nil]; + registerPath(appPath, YES, YES); + } + else + { + deleteSuc = systemMethodSuccessful; + } } if(deleteSuc) @@ -774,7 +828,7 @@ int uninstallApp(NSString* appPath, NSString* appId) } } -int uninstallAppByPath(NSString* appPath) +int uninstallAppByPath(NSString* appPath, BOOL useCustomMethod) { if(!appPath) return 1; @@ -786,21 +840,20 @@ int uninstallAppByPath(NSString* appPath) } NSString* appId = appIdForAppPath(standardizedAppPath); - return uninstallApp(appPath, appId); + return uninstallApp(appPath, appId, useCustomMethod); } -int uninstallAppById(NSString* appId) +int uninstallAppById(NSString* appId, BOOL useCustomMethod) { if(!appId) return 1; NSString* appPath = appPathForAppId(appId); if(!appPath) return 1; - return uninstallApp(appPath, appId); + return uninstallApp(appPath, appId, useCustomMethod); } // 166: IPA does not exist or is not accessible // 167: IPA does not appear to contain an app - -int installIpa(NSString* ipaPath, BOOL force) +int installIpa(NSString* ipaPath, BOOL force, BOOL useInstalldMethod) { cleanRestrictions(); @@ -819,18 +872,18 @@ int installIpa(NSString* ipaPath, BOOL force) return 168; } - int ret = installApp(tmpPackagePath, YES, force, NO); + int ret = installApp(tmpPackagePath, YES, force, NO, useInstalldMethod); [[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil]; return ret; } -void uninstallAllApps(void) +void uninstallAllApps(BOOL useCustomMethod) { for(NSString* appPath in trollStoreInstalledAppBundlePaths()) { - uninstallAppById(appIdForAppPath(appPath)); + uninstallAppById(appIdForAppPath(appPath), useCustomMethod); } } @@ -872,14 +925,36 @@ int installTrollStore(NSString* pathToTar) NSString* tmpTrollStorePath = [tmpPayloadPath stringByAppendingPathComponent:@"TrollStore.app"]; if(![[NSFileManager defaultManager] fileExistsAtPath:tmpTrollStorePath]) return 1; - // Save existing ldid installation if it exists - NSString* existingLdidPath = [trollStoreAppPath() stringByAppendingPathComponent:@"ldid"]; - if([[NSFileManager defaultManager] fileExistsAtPath:existingLdidPath]) + // Transfer existing ldid installation if it exists + // But only if the to-be-installed version of TrollStore is 1.5.0 or above + // This is to make it possible to downgrade to older versions still + + NSString* toInstallInfoPlistPath = [tmpTrollStorePath stringByAppendingPathComponent:@"Info.plist"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:toInstallInfoPlistPath]) return 1; + + NSDictionary* toInstallInfoDict = [NSDictionary dictionaryWithContentsOfFile:toInstallInfoPlistPath]; + NSString* toInstallVersion = toInstallInfoDict[@"CFBundleVersion"]; + + NSComparisonResult result = [@"1.5.0" compare:toInstallVersion options:NSNumericSearch]; + if(result != NSOrderedDescending) { - NSString* tmpLdidPath = [tmpTrollStorePath stringByAppendingPathComponent:@"ldid"]; - if(![[NSFileManager defaultManager] fileExistsAtPath:tmpLdidPath]) + NSString* existingLdidPath = [trollStoreAppPath() stringByAppendingPathComponent:@"ldid"]; + NSString* existingLdidVersionPath = [trollStoreAppPath() stringByAppendingPathComponent:@"ldid.version"]; + if([[NSFileManager defaultManager] fileExistsAtPath:existingLdidPath]) { - [[NSFileManager defaultManager] copyItemAtPath:existingLdidPath toPath:tmpLdidPath error:nil]; + NSString* tmpLdidPath = [tmpTrollStorePath stringByAppendingPathComponent:@"ldid"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:tmpLdidPath]) + { + [[NSFileManager defaultManager] copyItemAtPath:existingLdidPath toPath:tmpLdidPath error:nil]; + } + } + if([[NSFileManager defaultManager] fileExistsAtPath:existingLdidVersionPath]) + { + NSString* tmpLdidVersionPath = [tmpTrollStorePath stringByAppendingPathComponent:@"ldid.version"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:tmpLdidVersionPath]) + { + [[NSFileManager defaultManager] copyItemAtPath:existingLdidVersionPath toPath:tmpLdidVersionPath error:nil]; + } } } @@ -898,7 +973,7 @@ int installTrollStore(NSString* pathToTar) _installPersistenceHelper(persistenceHelperApp, trollStorePersistenceHelper, trollStoreRootHelper); } - int ret = installApp(tmpPackagePath, NO, YES, YES); + int ret = installApp(tmpPackagePath, NO, YES, YES, YES); NSLog(@"[installTrollStore] installApp => %d", ret); [[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil]; return ret; @@ -1106,90 +1181,114 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) @autoreleasepool { if(argc <= 1) return -1; - NSLog(@"trollstore helper go, uid: %d, gid: %d", getuid(), getgid()); + if(getuid() != 0) + { + NSLog(@"ERROR: trollstorehelper has to be run as root."); + return -1; + } + + NSMutableArray* args = [NSMutableArray new]; + for (int i = 1; i < argc; i++) + { + [args addObject:[NSString stringWithUTF8String:argv[i]]]; + } + + NSLog(@"trollstorehelper invoked with arguments: %@", args); int ret = 0; - - NSString* cmd = [NSString stringWithUTF8String:argv[1]]; + NSString* cmd = args.firstObject; if([cmd isEqualToString:@"install"]) { - BOOL force = NO; - if(argc <= 2) return -3; - if(argc > 3) - { - if(!strcmp(argv[3], "force")) - { - force = YES; - } - } - NSString* ipaPath = [NSString stringWithUTF8String:argv[2]]; - ret = installIpa(ipaPath, force); - } else if([cmd isEqualToString:@"uninstall"]) + if(args.count < 2) return -3; + // use system method when specified, otherwise use custom method + BOOL useInstalldMethod = [args containsObject:@"installd"]; + BOOL force = [args containsObject:@"force"]; + NSString* ipaPath = args.lastObject; + ret = installIpa(ipaPath, force, useInstalldMethod); + } + else if([cmd isEqualToString:@"uninstall"]) { - if(argc <= 2) return -3; - NSString* appId = [NSString stringWithUTF8String:argv[2]]; - ret = uninstallAppById(appId); - } else if([cmd isEqualToString:@"uninstall-path"]) + if(args.count < 2) return -3; + // use custom method when specified, otherwise use system method + BOOL useCustomMethod = [args containsObject:@"custom"]; + NSString* appId = args.lastObject; + ret = uninstallAppById(appId, useCustomMethod); + } + else if([cmd isEqualToString:@"uninstall-path"]) { - if(argc <= 2) return -3; - NSString* appPath = [NSString stringWithUTF8String:argv[2]]; - ret = uninstallAppByPath(appPath); - }else if([cmd isEqualToString:@"install-trollstore"]) + if(args.count < 2) return -3; + // use custom method when specified, otherwise use system method + BOOL useCustomMethod = [args containsObject:@"custom"]; + NSString* appPath = args.lastObject; + ret = uninstallAppByPath(appPath, useCustomMethod); + } + else if([cmd isEqualToString:@"install-trollstore"]) { - if(argc <= 2) return -3; - NSString* tsTar = [NSString stringWithUTF8String:argv[2]]; + if(args.count < 2) return -3; + NSString* tsTar = args.lastObject; ret = installTrollStore(tsTar); NSLog(@"installed troll store? %d", ret==0); - } else if([cmd isEqualToString:@"uninstall-trollstore"]) + } + else if([cmd isEqualToString:@"uninstall-trollstore"]) { - uninstallAllApps(); + if(![args containsObject:@"preserve-apps"]) + { + uninstallAllApps([args containsObject:@"custom"]); + } uninstallTrollStore(YES); - } else if([cmd isEqualToString:@"uninstall-trollstore-preserve-apps"]) + } + else if([cmd isEqualToString:@"install-ldid"]) { - uninstallTrollStore(YES); - }else if([cmd isEqualToString:@"install-ldid"]) - { - if(argc <= 2) return -3; - NSString* ldidPath = [NSString stringWithUTF8String:argv[2]]; - installLdid(ldidPath); - } else if([cmd isEqualToString:@"refresh"]) + if(args.count < 3) return -3; + NSString* ldidPath = args[1]; + NSString* ldidVersion = args[2]; + installLdid(ldidPath, ldidVersion); + } + else if([cmd isEqualToString:@"refresh"]) { refreshAppRegistrations(YES); - } else if([cmd isEqualToString:@"refresh-all"]) + } + else if([cmd isEqualToString:@"refresh-all"]) { cleanRestrictions(); //refreshAppRegistrations(NO); // <- fixes app permissions resetting, causes apps to move around on home screen, so I had to disable it + [[NSFileManager defaultManager] removeItemAtPath:@"/var/containers/Shared/SystemGroup/systemgroup.com.apple.lsd.iconscache/Library/Caches/com.apple.IconsCache" error:nil]; [[LSApplicationWorkspace defaultWorkspace] _LSPrivateRebuildApplicationDatabasesForSystemApps:YES internal:YES user:YES]; refreshAppRegistrations(YES); killall(@"backboardd", YES); - } else if([cmd isEqualToString:@"install-persistence-helper"]) + } + else if([cmd isEqualToString:@"install-persistence-helper"]) { - if(argc <= 2) return -3; - NSString* systemAppId = [NSString stringWithUTF8String:argv[2]]; + if(args.count < 2) return -3; + NSString* systemAppId = args.lastObject; installPersistenceHelper(systemAppId); - } else if([cmd isEqualToString:@"uninstall-persistence-helper"]) + } + else if([cmd isEqualToString:@"uninstall-persistence-helper"]) { uninstallPersistenceHelper(); - } else if([cmd isEqualToString:@"register-user-persistence-helper"]) + } + else if([cmd isEqualToString:@"register-user-persistence-helper"]) { - if(argc <= 2) return -3; - NSString* userAppId = [NSString stringWithUTF8String:argv[2]]; + if(args.count < 2) return -3; + NSString* userAppId = args.lastObject; registerUserPersistenceHelper(userAppId); - } else if([cmd isEqualToString:@"modify-registration"]) + } + else if([cmd isEqualToString:@"modify-registration"]) { - if(argc <= 3) return -3; - NSString* appPath = [NSString stringWithUTF8String:argv[2]]; - NSString* newRegistration = [NSString stringWithUTF8String:argv[3]]; + if(args.count < 3) return -3; + NSString* appPath = args[1]; + NSString* newRegistration = args[2]; NSString* trollStoreMark = [[appPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"_TrollStore"]; if([[NSFileManager defaultManager] fileExistsAtPath:trollStoreMark]) { registerPath(appPath, NO, [newRegistration isEqualToString:@"System"]); } - } else if([cmd isEqualToString:@"url-scheme"]) + } + else if([cmd isEqualToString:@"url-scheme"]) { - if(argc <= 2) return -3; - NSString* modifyArg = [NSString stringWithUTF8String:argv[2]]; + if(args.count < 2) return -3; + NSString* modifyArg = args.lastObject; BOOL newState = [modifyArg isEqualToString:@"enable"]; if(newState == YES || [modifyArg isEqualToString:@"disable"]) { @@ -1197,8 +1296,7 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) } } - NSLog(@"returning %d", ret); - + NSLog(@"trollstorehelper returning %d", ret); return ret; } } diff --git a/Shared/TSListControllerShared.h b/Shared/TSListControllerShared.h index cb9612d..f87220f 100644 --- a/Shared/TSListControllerShared.h +++ b/Shared/TSListControllerShared.h @@ -12,5 +12,6 @@ - (void)refreshAppRegistrationsPressed; - (void)uninstallPersistenceHelperPressed; - (void)handleUninstallation; +- (NSMutableArray*)argsForUninstallingTrollStore; - (void)uninstallTrollStorePressed; @end \ No newline at end of file diff --git a/Shared/TSListControllerShared.m b/Shared/TSListControllerShared.m index f57a1be..b6d827e 100644 --- a/Shared/TSListControllerShared.m +++ b/Shared/TSListControllerShared.m @@ -188,20 +188,28 @@ } } +- (NSMutableArray*)argsForUninstallingTrollStore +{ + return @[@"uninstall-trollstore"].mutableCopy; +} + - (void)uninstallTrollStorePressed { UIAlertController* uninstallAlert = [UIAlertController alertControllerWithTitle:@"Uninstall" message:@"You are about to uninstall TrollStore, do you want to preserve the apps installed by it?" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* uninstallAllAction = [UIAlertAction actionWithTitle:@"Uninstall TrollStore, Uninstall Apps" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { - spawnRoot(rootHelperPath(), @[@"uninstall-trollstore"], nil, nil); + NSMutableArray* args = [self argsForUninstallingTrollStore]; + spawnRoot(rootHelperPath(), @[args], nil, nil); [self handleUninstallation]; }]; [uninstallAlert addAction:uninstallAllAction]; UIAlertAction* preserveAppsAction = [UIAlertAction actionWithTitle:@"Uninstall TrollStore, Preserve Apps" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { - spawnRoot(rootHelperPath(), @[@"uninstall-trollstore-preserve-apps"], nil, nil); + NSMutableArray* args = [self argsForUninstallingTrollStore]; + [args addObject:@"preserve-apps"]; + spawnRoot(rootHelperPath(), args, nil, nil); [self handleUninstallation]; }]; [uninstallAlert addAction:preserveAppsAction]; diff --git a/Shared/TSUtil.h b/Shared/TSUtil.h index 38dede7..5871fef 100644 --- a/Shared/TSUtil.h +++ b/Shared/TSUtil.h @@ -13,6 +13,7 @@ extern int spawnRoot(NSString* path, NSArray* args, NSString** stdOut, NSString* extern void killall(NSString* processName, BOOL softly); extern void respring(void); extern void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)); +extern void fetchLatestLdidVersion(void (^completionHandler)(NSString* latestVersion)); extern NSArray* trollStoreInstalledAppBundlePaths(); extern NSArray* trollStoreInstalledAppContainerPaths(); diff --git a/Shared/TSUtil.m b/Shared/TSUtil.m index 5c38262..5b116ca 100644 --- a/Shared/TSUtil.m +++ b/Shared/TSUtil.m @@ -287,9 +287,10 @@ void respring(void) exit(0); } -void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)) +void github_fetchLatestVersion(NSString* repo, void (^completionHandler)(NSString* latestVersion)) { - NSURL* githubLatestAPIURL = [NSURL URLWithString:@"https://api.github.com/repos/opa334/TrollStore/releases/latest"]; + NSString* urlString = [NSString stringWithFormat:@"https://api.github.com/repos/%@/releases/latest", repo]; + NSURL* githubLatestAPIURL = [NSURL URLWithString:urlString]; NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithURL:githubLatestAPIURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { @@ -311,6 +312,16 @@ void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVers [task resume]; } +void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)) +{ + github_fetchLatestVersion(@"opa334/TrollStore", completionHandler); +} + +void fetchLatestLdidVersion(void (^completionHandler)(NSString* latestVersion)) +{ + github_fetchLatestVersion(@"opa334/ldid", completionHandler); +} + NSArray* trollStoreInstalledAppContainerPaths() { NSMutableArray* appContainerPaths = [NSMutableArray new]; diff --git a/TrollStore/TSApplicationsManager.m b/TrollStore/TSApplicationsManager.m index 14206b6..20d4357 100644 --- a/TrollStore/TSApplicationsManager.m +++ b/TrollStore/TSApplicationsManager.m @@ -1,5 +1,6 @@ #import "TSApplicationsManager.h" #import +extern NSUserDefaults* trollStoreUserDefaults(); @implementation TSApplicationsManager @@ -75,15 +76,25 @@ - (int)installIpa:(NSString*)pathToIpa force:(BOOL)force log:(NSString**)logOut { - int ret; + NSMutableArray* args = [NSMutableArray new]; + [args addObject:@"install"]; if(force) { - ret = spawnRoot(rootHelperPath(), @[@"install", pathToIpa, @"force"], nil, logOut); + [args addObject:@"force"]; + } + NSNumber* installationMethodToUseNum = [trollStoreUserDefaults() objectForKey:@"installationMethod"]; + int installationMethodToUse = installationMethodToUseNum ? installationMethodToUseNum.intValue : 1; + if(installationMethodToUse == 1) + { + [args addObject:@"custom"]; } else { - ret = spawnRoot(rootHelperPath(), @[@"install", pathToIpa], nil, logOut); + [args addObject:@"installd"]; } + [args addObject:pathToIpa]; + + int ret = spawnRoot(rootHelperPath(), args, nil, logOut); [[NSNotificationCenter defaultCenter] postNotificationName:@"ApplicationsChanged" object:nil]; return ret; } @@ -96,7 +107,24 @@ - (int)uninstallApp:(NSString*)appId { if(!appId) return -200; - int ret = spawnRoot(rootHelperPath(), @[@"uninstall", appId], nil, nil); + + NSMutableArray* args = [NSMutableArray new]; + [args addObject:@"uninstall"]; + + NSNumber* uninstallationMethodToUseNum = [trollStoreUserDefaults() objectForKey:@"uninstallationMethod"]; + int uninstallationMethodToUse = uninstallationMethodToUseNum ? uninstallationMethodToUseNum.intValue : 0; + if(uninstallationMethodToUse == 1) + { + [args addObject:@"custom"]; + } + else + { + [args addObject:@"installd"]; + } + + [args addObject:appId]; + + int ret = spawnRoot(rootHelperPath(), args, nil, nil); [[NSNotificationCenter defaultCenter] postNotificationName:@"ApplicationsChanged" object:nil]; return ret; } @@ -104,7 +132,24 @@ - (int)uninstallAppByPath:(NSString*)path { if(!path) return -200; - int ret = spawnRoot(rootHelperPath(), @[@"uninstall-path", path], nil, nil); + + NSMutableArray* args = [NSMutableArray new]; + [args addObject:@"uninstall-path"]; + + NSNumber* uninstallationMethodToUseNum = [trollStoreUserDefaults() objectForKey:@"uninstallationMethod"]; + int uninstallationMethodToUse = uninstallationMethodToUseNum ? uninstallationMethodToUseNum.intValue : 0; + if(uninstallationMethodToUse == 1) + { + [args addObject:@"custom"]; + } + else + { + [args addObject:@"installd"]; + } + + [args addObject:path]; + + int ret = spawnRoot(rootHelperPath(), args, nil, nil); [[NSNotificationCenter defaultCenter] postNotificationName:@"ApplicationsChanged" object:nil]; return ret; } diff --git a/TrollStore/TSInstallationController.h b/TrollStore/TSInstallationController.h index 96ebdc2..0c64233 100644 --- a/TrollStore/TSInstallationController.h +++ b/TrollStore/TSInstallationController.h @@ -9,4 +9,6 @@ + (void)handleAppInstallFromRemoteURL:(NSURL*)remoteURL completion:(void (^)(BOOL, NSError*))completion; ++ (void)installLdid; + @end \ No newline at end of file diff --git a/TrollStore/TSInstallationController.m b/TrollStore/TSInstallationController.m index 9fedb61..dee437f 100644 --- a/TrollStore/TSInstallationController.m +++ b/TrollStore/TSInstallationController.m @@ -187,4 +187,47 @@ extern NSUserDefaults* trollStoreUserDefaults(void); }); } ++ (void)installLdid +{ + fetchLatestLdidVersion(^(NSString* latestVersion) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + NSURL* ldidURL = [NSURL URLWithString:@"https://github.com/opa334/ldid/releases/latest/download/ldid"]; + NSURLRequest* ldidRequest = [NSURLRequest requestWithURL:ldidURL]; + + [TSPresentationDelegate startActivity:@"Installing ldid"]; + + NSURLSessionDownloadTask* downloadTask = [NSURLSession.sharedSession downloadTaskWithRequest:ldidRequest completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) + { + if(error) + { + UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"Error downloading ldid: %@", error] preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [errorAlert addAction:closeAction]; + + dispatch_async(dispatch_get_main_queue(), ^ + { + [TSPresentationDelegate stopActivityWithCompletion:^ + { + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; + }]; + }); + } + else + { + spawnRoot(rootHelperPath(), @[@"install-ldid", location.path, latestVersion], nil, nil); + dispatch_async(dispatch_get_main_queue(), ^ + { + [TSPresentationDelegate stopActivityWithCompletion:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"TrollStoreReloadSettingsNotification" object:nil userInfo:nil]; + }); + } + }]; + + [downloadTask resume]; + }); + }); +} + @end \ No newline at end of file diff --git a/TrollStore/TSSceneDelegate.m b/TrollStore/TSSceneDelegate.m index 8550dfe..4a6fa91 100644 --- a/TrollStore/TSSceneDelegate.m +++ b/TrollStore/TSSceneDelegate.m @@ -72,6 +72,20 @@ } } +// We want to auto install ldid if either it doesn't exist +// or if it's the one from an old TrollStore version that's no longer supported +- (void)handleLdidCheck +{ + NSString* tsAppPath = [NSBundle mainBundle].bundlePath; + + NSString* ldidPath = [tsAppPath stringByAppendingPathComponent:@"ldid"]; + NSString* ldidVersionPath = [tsAppPath stringByAppendingPathComponent:@"ldid.version"]; + + if(![[NSFileManager defaultManager] fileExistsAtPath:ldidPath] || ![[NSFileManager defaultManager] fileExistsAtPath:ldidVersionPath]) + { + [TSInstallationController installLdid]; + } +} - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. @@ -88,6 +102,10 @@ { [self handleURLContexts:connectionOptions.URLContexts scene:(UIWindowScene*)scene]; } + else + { + [self handleLdidCheck]; + } } diff --git a/TrollStore/TSSettingsAdvancedListController.h b/TrollStore/TSSettingsAdvancedListController.h new file mode 100644 index 0000000..0721751 --- /dev/null +++ b/TrollStore/TSSettingsAdvancedListController.h @@ -0,0 +1,5 @@ +#import + +@interface TSSettingsAdvancedListController : PSListController + +@end \ No newline at end of file diff --git a/TrollStore/TSSettingsAdvancedListController.m b/TrollStore/TSSettingsAdvancedListController.m new file mode 100644 index 0000000..97bc582 --- /dev/null +++ b/TrollStore/TSSettingsAdvancedListController.m @@ -0,0 +1,103 @@ +#import "TSSettingsAdvancedListController.h" +#import + +extern NSUserDefaults* trollStoreUserDefaults(); +@interface PSSpecifier () +@property (nonatomic,retain) NSArray* values; +@end + +@implementation TSSettingsAdvancedListController + +- (NSMutableArray*)specifiers +{ + if(!_specifiers) + { + _specifiers = [NSMutableArray new]; + + PSSpecifier* installationMethodGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; + //installationMethodGroupSpecifier.name = @"Installation"; + [installationMethodGroupSpecifier setProperty:@"installd:\nInstalls applications by doing a placeholder installation through installd, fixing the permissions and then adding it to icon cache.\nAdvantage: Might be slightly more persistent then the custom method in terms of icon cache reloads.\nDisadvantage: Causes some small issues with certain applications for seemingly no reason (E.g. Watusi cannot save preferences when being installed using this method).\n\nCustom (Recommended):\nInstalls applications by manually creating a bundle using MobileContainerManager, copying the app into it and adding it to icon cache.\nAdvantage: No known issues (As opposed to the Watusi issue outlined in the installd method).\nDisadvantage: Might be slightly less persistent then the installd method in terms of icon cache reloads.\n\nNOTE: In cases where installd is selected but the placeholder installation fails, TrollStore automatically falls back to using the Custom method." forKey:@"footerText"]; + [_specifiers addObject:installationMethodGroupSpecifier]; + + PSSpecifier* installationMethodSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Installation Method" + target:self + set:nil + get:nil + detail:nil + cell:PSStaticTextCell + edit:nil]; + [installationMethodSpecifier setProperty:@YES forKey:@"enabled"]; + installationMethodSpecifier.identifier = @"installationMethodLabel"; + [_specifiers addObject:installationMethodSpecifier]; + + PSSpecifier* installationMethodSegmentSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Installation Method Segment" + target:self + set:@selector(setPreferenceValue:specifier:) + get:@selector(readPreferenceValue:) + detail:nil + cell:PSSegmentCell + edit:nil]; + [installationMethodSegmentSpecifier setProperty:@YES forKey:@"enabled"]; + installationMethodSegmentSpecifier.identifier = @"installationMethodSegment"; + [installationMethodSegmentSpecifier setProperty:@"com.opa334.TrollStore" forKey:@"defaults"]; + [installationMethodSegmentSpecifier setProperty:@"installationMethod" forKey:@"key"]; + installationMethodSegmentSpecifier.values = @[@0, @1]; + installationMethodSegmentSpecifier.titleDictionary = @{@0 : @"installd", @1 : @"Custom"}; + [installationMethodSegmentSpecifier setProperty:@1 forKey:@"default"]; + [_specifiers addObject:installationMethodSegmentSpecifier]; + + PSSpecifier* uninstallationMethodGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; + //uninstallationMethodGroupSpecifier.name = @"Uninstallation"; + [uninstallationMethodGroupSpecifier setProperty:@"installd (Recommended):\nUninstalls applications using the same API that SpringBoard uses when uninstalling them from the home screen.\n\nCustom:\nUninstalls applications by removing them from icon cache and then deleting their application and data bundles directly.\n\nNOTE: In cases where installd is selected but the stock uninstallation fails, TrollStore automatically falls back to using the Custom method." forKey:@"footerText"]; + [_specifiers addObject:uninstallationMethodGroupSpecifier]; + + PSSpecifier* uninstallationMethodSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Uninstallation Method" + target:self + set:nil + get:nil + detail:nil + cell:PSStaticTextCell + edit:nil]; + [uninstallationMethodSpecifier setProperty:@YES forKey:@"enabled"]; + uninstallationMethodSpecifier.identifier = @"uninstallationMethodLabel"; + [_specifiers addObject:uninstallationMethodSpecifier]; + + PSSpecifier* uninstallationMethodSegmentSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Installation Method Segment" + target:self + set:@selector(setPreferenceValue:specifier:) + get:@selector(readPreferenceValue:) + detail:nil + cell:PSSegmentCell + edit:nil]; + [uninstallationMethodSegmentSpecifier setProperty:@YES forKey:@"enabled"]; + uninstallationMethodSegmentSpecifier.identifier = @"uninstallationMethodSegment"; + [uninstallationMethodSegmentSpecifier setProperty:@"com.opa334.TrollStore" forKey:@"defaults"]; + [uninstallationMethodSegmentSpecifier setProperty:@"uninstallationMethod" forKey:@"key"]; + uninstallationMethodSegmentSpecifier.values = @[@0, @1]; + uninstallationMethodSegmentSpecifier.titleDictionary = @{@0 : @"installd", @1 : @"Custom"}; + [uninstallationMethodSegmentSpecifier setProperty:@0 forKey:@"default"]; + [_specifiers addObject:uninstallationMethodSegmentSpecifier]; + } + + [(UINavigationItem *)self.navigationItem setTitle:@"Advanced"]; + return _specifiers; +} + +- (void)setPreferenceValue:(NSObject*)value specifier:(PSSpecifier*)specifier +{ + NSUserDefaults* tsDefaults = trollStoreUserDefaults(); + [tsDefaults setObject:value forKey:[specifier propertyForKey:@"key"]]; +} + +- (NSObject*)readPreferenceValue:(PSSpecifier*)specifier +{ + NSUserDefaults* tsDefaults = trollStoreUserDefaults(); + NSObject* toReturn = [tsDefaults objectForKey:[specifier propertyForKey:@"key"]]; + if(!toReturn) + { + toReturn = [specifier propertyForKey:@"default"]; + } + return toReturn; +} + +@end \ No newline at end of file diff --git a/TrollStore/TSSettingsListController.h b/TrollStore/TSSettingsListController.h index 959d405..fc573ee 100644 --- a/TrollStore/TSSettingsListController.h +++ b/TrollStore/TSSettingsListController.h @@ -4,5 +4,6 @@ { PSSpecifier* _installPersistenceHelperSpecifier; NSString* _newerVersion; + NSString* _newerLdidVersion; } @end \ No newline at end of file diff --git a/TrollStore/TSSettingsListController.m b/TrollStore/TSSettingsListController.m index 6782f9f..8d2bec1 100644 --- a/TrollStore/TSSettingsListController.m +++ b/TrollStore/TSSettingsListController.m @@ -3,6 +3,8 @@ #import #import #import +#import "TSInstallationController.h" +#import "TSSettingsAdvancedListController.h" @interface NSUserDefaults (Private) - (instancetype)_initWithSuiteName:(NSString *)suiteName container:(NSURL *)container; @@ -15,6 +17,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadSpecifiers) name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadSpecifiers) name:@"TrollStoreReloadSettingsNotification" object:nil]; fetchLatestTrollStoreVersion(^(NSString* latestVersion) { @@ -29,6 +32,26 @@ extern NSUserDefaults* trollStoreUserDefaults(void); }); } }); + + fetchLatestLdidVersion(^(NSString* latestVersion) + { + NSString* ldidVersionPath = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"ldid.version"]; + NSString* ldidVersion = nil; + NSData* ldidVersionData = [NSData dataWithContentsOfFile:ldidVersionPath]; + if(ldidVersionData) + { + ldidVersion = [[NSString alloc] initWithData:ldidVersionData encoding:NSUTF8StringEncoding]; + } + + if(![latestVersion isEqualToString:ldidVersion]) + { + _newerLdidVersion = latestVersion; + dispatch_async(dispatch_get_main_queue(), ^ + { + [self reloadSpecifiers]; + }); + } + }); } - (NSMutableArray*)specifiers @@ -88,8 +111,16 @@ extern NSUserDefaults* trollStoreUserDefaults(void); [_specifiers addObject:rebuildIconCacheSpecifier]; NSString* ldidPath = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"ldid"]; + NSString* ldidVersionPath = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:@"ldid.version"]; BOOL ldidInstalled = [[NSFileManager defaultManager] fileExistsAtPath:ldidPath]; + NSString* ldidVersion = nil; + NSData* ldidVersionData = [NSData dataWithContentsOfFile:ldidVersionPath]; + if(ldidVersionData) + { + ldidVersion = [[NSString alloc] initWithData:ldidVersionData encoding:NSUTF8StringEncoding]; + } + PSSpecifier* signingGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; signingGroupSpecifier.name = @"Signing"; @@ -106,7 +137,13 @@ extern NSUserDefaults* trollStoreUserDefaults(void); if(ldidInstalled) { - PSSpecifier* ldidInstalledSpecifier = [PSSpecifier preferenceSpecifierNamed:@"ldid: Installed" + NSString* installedTitle = @"ldid: Installed"; + if(ldidVersion) + { + installedTitle = [NSString stringWithFormat:@"%@ (%@)", installedTitle, ldidVersion]; + } + + PSSpecifier* ldidInstalledSpecifier = [PSSpecifier preferenceSpecifierNamed:installedTitle target:self set:nil get:nil @@ -116,6 +153,22 @@ extern NSUserDefaults* trollStoreUserDefaults(void); [ldidInstalledSpecifier setProperty:@NO forKey:@"enabled"]; ldidInstalledSpecifier.identifier = @"ldidInstalled"; [_specifiers addObject:ldidInstalledSpecifier]; + + if(_newerLdidVersion && ![_newerLdidVersion isEqualToString:ldidVersion]) + { + NSString* updateTitle = [NSString stringWithFormat:@"Update to %@", _newerLdidVersion]; + PSSpecifier* ldidUpdateSpecifier = [PSSpecifier preferenceSpecifierNamed:updateTitle + target:self + set:nil + get:nil + detail:nil + cell:PSButtonCell + edit:nil]; + ldidUpdateSpecifier.identifier = @"updateLdid"; + [ldidUpdateSpecifier setProperty:@YES forKey:@"enabled"]; + ldidUpdateSpecifier.buttonAction = @selector(installOrUpdateLdidPressed); + [_specifiers addObject:ldidUpdateSpecifier]; + } } else { @@ -126,9 +179,9 @@ extern NSUserDefaults* trollStoreUserDefaults(void); detail:nil cell:PSButtonCell edit:nil]; - installLdidSpecifier.identifier = @"ldidInstalled"; + installLdidSpecifier.identifier = @"installLdid"; [installLdidSpecifier setProperty:@YES forKey:@"enabled"]; - installLdidSpecifier.buttonAction = @selector(installLdidPressed); + installLdidSpecifier.buttonAction = @selector(installOrUpdateLdidPressed); [_specifiers addObject:installLdidSpecifier]; } @@ -234,11 +287,21 @@ extern NSUserDefaults* trollStoreUserDefaults(void); [_specifiers addObject:installAlertConfigurationSpecifier]; - PSSpecifier* otherGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; - [otherGroupSpecifier setProperty:[NSString stringWithFormat:@"TrollStore %@\n\n© 2022 Lars Fröder (opa334)\n\nCredits:\n@LinusHenze: CoreTrust bug\n@zhuowei: CoreTrust bug writeup and cert\n@lunotech11, @SerenaKit, @tylinux: Various contributions\n@ProcursusTeam: uicache and ldid build\n@cstar_ow: uicache\n@saurik: ldid", [self getTrollStoreVersion]] forKey:@"footerText"]; + [otherGroupSpecifier setProperty:[NSString stringWithFormat:@"TrollStore %@\n\n© 2022 Lars Fröder (opa334)\n\nTrollStore is NOT for piracy!\n\nCredits:\n@LinusHenze: CoreTrust bug\n@zhuowei: CoreTrust bug writeup and cert\n@lunotech11, @SerenaKit, @tylinux: Various contributions\n@ProcursusTeam: uicache and ldid build\n@cstar_ow: uicache\n@saurik: ldid", [self getTrollStoreVersion]] forKey:@"footerText"]; [_specifiers addObject:otherGroupSpecifier]; + PSSpecifier* advancedLinkSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Advanced" + target:self + set:nil + get:nil + detail:nil + cell:PSLinkListCell + edit:nil]; + advancedLinkSpecifier.detailControllerClass = [TSSettingsAdvancedListController class]; + [advancedLinkSpecifier setProperty:@YES forKey:@"enabled"]; + [_specifiers addObject:advancedLinkSpecifier]; + // Uninstall TrollStore PSSpecifier* uninstallTrollStoreSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Uninstall TrollStore" target:self @@ -277,7 +340,7 @@ extern NSUserDefaults* trollStoreUserDefaults(void); - (NSArray*)installationConfirmationNames { - return @[@"Always (Recommended)", @"Only on Remote Installs", @"Never (Not Recommeded)"]; + return @[@"Always (Recommended)", @"Only on Remote URL Installs", @"Never (Not Recommeded)"]; } - (void)respringButtonPressed @@ -285,41 +348,9 @@ extern NSUserDefaults* trollStoreUserDefaults(void); respring(); } -- (void)installLdidPressed +- (void)installOrUpdateLdidPressed { - NSURL* ldidURL = [NSURL URLWithString:@"https://github.com/opa334/ldid/releases/download/v2.1.5-procursus5/ldid"]; - NSURLRequest* ldidRequest = [NSURLRequest requestWithURL:ldidURL]; - - [TSPresentationDelegate startActivity:@"Installing ldid"]; - - NSURLSessionDownloadTask* downloadTask = [NSURLSession.sharedSession downloadTaskWithRequest:ldidRequest completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) - { - if(error) - { - UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"Error downloading ldid: %@", error] preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; - [errorAlert addAction:closeAction]; - - dispatch_async(dispatch_get_main_queue(), ^ - { - [TSPresentationDelegate stopActivityWithCompletion:^ - { - [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; - }]; - }); - } - else - { - spawnRoot(rootHelperPath(), @[@"install-ldid", location.path], nil, nil); - dispatch_async(dispatch_get_main_queue(), ^ - { - [TSPresentationDelegate stopActivityWithCompletion:nil]; - [self reloadSpecifiers]; - }); - } - }]; - - [downloadTask resume]; + [TSInstallationController installLdid]; } - (void)installPersistenceHelperPressed @@ -410,4 +441,18 @@ extern NSUserDefaults* trollStoreUserDefaults(void); return toReturn; } +- (NSMutableArray*)argsForUninstallingTrollStore +{ + NSMutableArray* args = @[@"uninstall-trollstore"].mutableCopy; + + NSNumber* uninstallationMethodToUseNum = [trollStoreUserDefaults() objectForKey:@"uninstallationMethod"]; + int uninstallationMethodToUse = uninstallationMethodToUseNum ? uninstallationMethodToUseNum.intValue : 0; + if(uninstallationMethodToUse == 1) + { + [args addObject:@"custom"]; + } + + return args; +} + @end \ No newline at end of file diff --git a/cert.p12 b/cert.p12 index 378b83b15ab4c7fb4e4657a8bd011606819bdcfd..a614168bd84340392def22ec93363a7340b8fe5d 100644 GIT binary patch literal 4870 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 Wi2La%wxM^zmB(*C8$lr0fPVm%rg4)1 literal 4901 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%0A%eY%p<3f6qO?%x;C$LR^Q>Vb=(pMb&Sswz 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 Wi2La%wxM^zmB(*C8$lr0fPVm%rg4)1