490 lines
20 KiB
Swift
490 lines
20 KiB
Swift
//
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import MobileCoin
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
#if USE_DEBUG_UI
|
|
|
|
class DebugUIMisc: NSObject, DebugUIPage, Dependencies {
|
|
|
|
let name = "Misc."
|
|
|
|
func section(thread: TSThread?) -> OWSTableSection? {
|
|
var items = [OWSTableItem]()
|
|
|
|
if let thread {
|
|
items.append(OWSTableItem(title: "Delete disappearing messages config", actionBlock: {
|
|
self.databaseStorage.write { transaction in
|
|
let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
|
|
dmConfigurationStore.remove(for: thread, tx: transaction.asV2Write)
|
|
}
|
|
}))
|
|
}
|
|
|
|
items += [
|
|
OWSTableItem(title: "Corrupt username", actionBlock: {
|
|
SDSDatabaseStorage.shared.write { tx in
|
|
DependenciesBridge.shared.localUsernameManager.setLocalUsernameCorrupted(tx: tx.asV2Write)
|
|
}
|
|
}),
|
|
|
|
OWSTableItem(title: "Reenable disabled inactive linked device reminder megaphones", actionBlock: {
|
|
SDSDatabaseStorage.shared.write { tx in
|
|
DependenciesBridge.shared.inactiveLinkedDeviceFinder
|
|
.reenablePermanentlyDisabledFinders(tx: tx.asV2Write)
|
|
}
|
|
}),
|
|
|
|
OWSTableItem(title: "Re-register", actionBlock: {
|
|
OWSActionSheets.showConfirmationAlert(
|
|
title: "Re-register?",
|
|
message: "If you proceed, you will not lose any of your current messages, " +
|
|
"but your account will be deactivated until you complete re-registration.",
|
|
proceedTitle: "Proceed",
|
|
proceedAction: { _ in
|
|
DebugUIMisc.reregister()
|
|
}
|
|
)
|
|
}),
|
|
|
|
OWSTableItem(title: "Show 2FA Reminder", actionBlock: {
|
|
DebugUIMisc.showPinReminder()
|
|
}),
|
|
OWSTableItem(title: "Reset 2FA Repetition Interval", actionBlock: {
|
|
SDSDatabaseStorage.shared.write { transaction in
|
|
OWS2FAManager.shared.setDefaultRepetitionIntervalWith(transaction)
|
|
}
|
|
}),
|
|
|
|
OWSTableItem(title: "Share UIImage", actionBlock: {
|
|
let image = UIImage.image(color: .red, size: .square(1))
|
|
AttachmentSharing.showShareUI(for: image)
|
|
}),
|
|
OWSTableItem(title: "Share 2 images", actionBlock: {
|
|
DebugUIMisc.shareImages(2)
|
|
}),
|
|
OWSTableItem(title: "Share 2 videos", actionBlock: {
|
|
DebugUIMisc.shareVideos(2)
|
|
}),
|
|
OWSTableItem(title: "Share 2 PDFs", actionBlock: {
|
|
DebugUIMisc.sharePDFs(2)
|
|
}),
|
|
|
|
OWSTableItem(title: "Fetch system contacts", actionBlock: {
|
|
SSKEnvironment.shared.contactsManagerImpl.requestSystemContactsOnce()
|
|
}),
|
|
OWSTableItem(title: "Flag database as corrupted", actionBlock: {
|
|
DebugUIMisc.showFlagDatabaseAsCorruptedUi()
|
|
}),
|
|
OWSTableItem(title: "Flag database as read corrupted", actionBlock: {
|
|
DebugUIMisc.showFlagDatabaseAsReadCorruptedUi()
|
|
}),
|
|
|
|
OWSTableItem(title: "Add 1k KV keys", actionBlock: {
|
|
DebugUIMisc.populateRandomKeyValueStores(keyCount: 1 * 1000)
|
|
}),
|
|
OWSTableItem(title: "Add 10k KV keys", actionBlock: {
|
|
DebugUIMisc.populateRandomKeyValueStores(keyCount: 10 * 1000)
|
|
}),
|
|
OWSTableItem(title: "Add 100k KV keys", actionBlock: {
|
|
DebugUIMisc.populateRandomKeyValueStores(keyCount: 100 * 1000)
|
|
}),
|
|
OWSTableItem(title: "Add 1m KV keys", actionBlock: {
|
|
DebugUIMisc.populateRandomKeyValueStores(keyCount: 1000 * 1000)
|
|
}),
|
|
OWSTableItem(title: "Clear Random KV keys", actionBlock: {
|
|
DebugUIMisc.clearRandomKeyValueStores()
|
|
}),
|
|
|
|
OWSTableItem(title: "Save plaintext database key", actionBlock: {
|
|
DebugUIMisc.enableExternalDatabaseAccess()
|
|
}),
|
|
|
|
OWSTableItem(title: "Update account attributes", actionBlock: {
|
|
Task {
|
|
try? await DependenciesBridge.shared.accountAttributesUpdater.updateAccountAttributes(authedAccount: .implicit())
|
|
}
|
|
}),
|
|
|
|
OWSTableItem(title: "Check Prekeys", actionBlock: {
|
|
guard let preKeyManagerImpl = DependenciesBridge.shared.preKeyManager as? PreKeyManagerImpl else {
|
|
return
|
|
}
|
|
self.databaseStorage.read { tx in
|
|
preKeyManagerImpl.checkPreKeysImmediately(tx: tx.asV2Read)
|
|
}
|
|
}),
|
|
OWSTableItem(title: "Remove All Prekeys", actionBlock: {
|
|
DebugUIMisc.removeAllPrekeys()
|
|
}),
|
|
OWSTableItem(title: "Remove All Sessions", actionBlock: {
|
|
DebugUIMisc.removeAllSessions()
|
|
}),
|
|
OWSTableItem(title: "Remove local PNI identity key", actionBlock: {
|
|
DebugUIMisc.removeLocalPniIdentityKey()
|
|
}),
|
|
|
|
OWSTableItem(title: "Log all sticker suggestions", actionBlock: {
|
|
DebugUIMisc.logStickerSuggestions()
|
|
}),
|
|
|
|
OWSTableItem(title: "Log Local Account", actionBlock: {
|
|
DebugUIMisc.logLocalAccount()
|
|
}),
|
|
|
|
OWSTableItem(title: "Log SignalRecipients", actionBlock: {
|
|
DebugUIMisc.logSignalRecipients()
|
|
}),
|
|
|
|
OWSTableItem(title: "Clear Profile Key Credentials", actionBlock: {
|
|
DebugUIMisc.clearProfileKeyCredentials()
|
|
}),
|
|
|
|
OWSTableItem(title: "Clear custom reaction emoji (locally)", actionBlock: {
|
|
DebugUIMisc.clearLocalCustomEmoji()
|
|
}),
|
|
|
|
OWSTableItem(title: "Clear My Story privacy settings", actionBlock: {
|
|
DebugUIMisc.clearMyStoryPrivacySettings()
|
|
}),
|
|
|
|
OWSTableItem(title: "Enable username education prompt", actionBlock: {
|
|
DebugUIMisc.enableUsernameEducation()
|
|
}),
|
|
|
|
OWSTableItem(title: "Enable username link tooltip", actionBlock: {
|
|
DebugUIMisc.enableUsernameLinkTooltip()
|
|
}),
|
|
|
|
OWSTableItem(title: "Delete all persisted ExperienceUpgrade records", actionBlock: {
|
|
DebugUIMisc.removeAllRecordedExperienceUpgrades()
|
|
}),
|
|
|
|
OWSTableItem(title: "Test spoiler animations", actionBlock: {
|
|
DebugUIMisc.showSpoilerAnimationTestController()
|
|
}),
|
|
|
|
OWSTableItem(title: "Mark flip cam button tooltip as unread", actionBlock: {
|
|
let flipCamTooltipManager = FlipCameraTooltipManager(db: DependenciesBridge.shared.db)
|
|
flipCamTooltipManager.markTooltipAsUnread()
|
|
}),
|
|
|
|
OWSTableItem(title: "Enable DeleteForMeSyncMessage info sheet", actionBlock: {
|
|
SDSDatabaseStorage.shared.write { tx in
|
|
DeleteForMeInfoSheetCoordinator.fromGlobals()
|
|
.forceEnableInfoSheet(tx: tx.asV2Write)
|
|
}
|
|
})
|
|
]
|
|
return OWSTableSection(title: name, items: items)
|
|
}
|
|
|
|
// MARK: Attachment Sharing
|
|
|
|
private static func sendAttachment(_ attachment: SignalAttachment, toThread thread: TSThread) {
|
|
guard !attachment.hasError else {
|
|
owsFailDebug("attachment[\(String(describing: attachment.sourceFilename))]: \(String(describing: attachment.errorName))")
|
|
return
|
|
}
|
|
ThreadUtil.enqueueMessage(
|
|
body: nil,
|
|
mediaAttachments: [ attachment ],
|
|
thread: thread
|
|
)
|
|
}
|
|
|
|
private static func shareAssets(_ count: UInt, fromAssetLoaders assetLoaders: [DebugUIMessagesAssetLoader]) {
|
|
DebugUIMessagesAssetLoader.prepareAssetLoaders(assetLoaders) { result in
|
|
switch result {
|
|
case .success:
|
|
DebugUIMisc.shareAssets(count, fromAssetLoaders: assetLoaders)
|
|
|
|
case .failure(let error):
|
|
Logger.error("Could not prepare asset loaders. \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func shareAssets(_ count: UInt, fromPreparedAssetLoaders assetLoaders: [DebugUIMessagesAssetLoader]) {
|
|
let shuffledAssetLoaders = assetLoaders.shuffled()
|
|
let urls: [URL] = shuffledAssetLoaders.compactMap { assetLoader in
|
|
guard let assetFilePath = assetLoader.filePath else {
|
|
owsFailDebug("assetLoader.filePath is nil")
|
|
return nil
|
|
}
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: assetFilePath.fileExtension)
|
|
do {
|
|
try FileManager.default.copyItem(atPath: assetFilePath, toPath: filePath)
|
|
return URL(fileURLWithPath: filePath)
|
|
} catch {
|
|
Logger.error("Error while copying asset at [\(assetFilePath)]: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
Logger.verbose("urls: \(urls)")
|
|
AttachmentSharing.showShareUI(for: urls, sender: nil, completion: nil)
|
|
}
|
|
|
|
private static func shareImages(_ count: UInt) {
|
|
shareAssets(count, fromAssetLoaders: [
|
|
DebugUIMessagesAssetLoader.jpegInstance,
|
|
DebugUIMessagesAssetLoader.tinyPngInstance
|
|
])
|
|
}
|
|
|
|
private static func shareVideos(_ count: UInt) {
|
|
shareAssets(count, fromAssetLoaders: [ DebugUIMessagesAssetLoader.mp4Instance ])
|
|
}
|
|
|
|
private static func sharePDFs(_ count: UInt) {
|
|
shareAssets(count, fromAssetLoaders: [ DebugUIMessagesAssetLoader.tinyPdfInstance ])
|
|
}
|
|
|
|
// MARK: KVS
|
|
|
|
private static func randomKeyValueStore() -> SDSKeyValueStore {
|
|
SDSKeyValueStore(collection: "randomKeyValueStore")
|
|
}
|
|
|
|
private static func populateRandomKeyValueStores(keyCount: UInt) {
|
|
let store = randomKeyValueStore()
|
|
|
|
let kBatchSize: UInt = 1000
|
|
let batchCount: UInt = keyCount / kBatchSize
|
|
Logger.verbose("keyCount: \(keyCount)")
|
|
Logger.verbose("batchCount: \(batchCount)")
|
|
for batchIndex in 0..<batchCount {
|
|
Logger.verbose("batchIndex: \(batchIndex) / \(batchCount)")
|
|
|
|
autoreleasepool {
|
|
databaseStorage.write { transaction in
|
|
// Set three values at a time.
|
|
for _ in 0..<kBatchSize / 3 {
|
|
let value = Randomness.generateRandomBytes(4096)
|
|
store.setData(value, key: UUID().uuidString, transaction: transaction)
|
|
store.setString(UUID().uuidString, key: UUID().uuidString, transaction: transaction)
|
|
store.setBool(Bool.random(), key: UUID().uuidString, transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func clearRandomKeyValueStores() {
|
|
let store = randomKeyValueStore()
|
|
databaseStorage.write { transcation in
|
|
store.removeAll(transaction: transcation)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static func reregister() {
|
|
Logger.info("Re-registering.")
|
|
RegistrationUtils.reregister(fromViewController: SignalApp.shared.conversationSplitViewController!)
|
|
}
|
|
|
|
private static func enableExternalDatabaseAccess() {
|
|
guard Platform.isSimulator else {
|
|
OWSActionSheets.showErrorAlert(message: "Must be running in the simulator")
|
|
return
|
|
}
|
|
OWSActionSheets.showConfirmationAlert(
|
|
title: "⚠️⚠️⚠️ Warning!!! ⚠️⚠️⚠️",
|
|
message: "This will save your database key in plaintext and severely weaken the security of " +
|
|
"all data. Make sure you're using a test account with data you don't care about.",
|
|
proceedTitle: "I'm okay with this",
|
|
proceedStyle: .destructive,
|
|
proceedAction: { _ in
|
|
debugOnly_savePlaintextDbKey()
|
|
}
|
|
)
|
|
}
|
|
|
|
static func debugOnly_savePlaintextDbKey() {
|
|
#if TESTABLE_BUILD && targetEnvironment(simulator)
|
|
// Note: These static strings go hand-in-hand with Scripts/sqlclient.py
|
|
let payload = [ "key": NSObject.databaseStorage.keyFetcher.debugOnly_keyData()?.hexadecimalString ]
|
|
let payloadData = try! JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted)
|
|
|
|
let groupDir = URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath(), isDirectory: true)
|
|
let destURL = groupDir.appendingPathComponent("dbPayload.txt")
|
|
try! payloadData.write(to: destURL, options: .atomic)
|
|
#else
|
|
// This should be caught above. Fatal assert just in case.
|
|
owsFail("Can't savePlaintextDbKey")
|
|
#endif
|
|
}
|
|
|
|
private static func removeAllPrekeys() {
|
|
databaseStorage.write { transaction in
|
|
let signalProtoclStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
|
|
let signalProtocolStoreACI = signalProtoclStoreManager.signalProtocolStore(for: .aci)
|
|
signalProtocolStoreACI.signedPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStoreACI.preKeyStore.removeAll(tx: transaction.asV2Write)
|
|
|
|
let signalProtocolStorePNI = signalProtoclStoreManager.signalProtocolStore(for: .pni)
|
|
signalProtocolStorePNI.signedPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStorePNI.preKeyStore.removeAll(tx: transaction.asV2Write)
|
|
}
|
|
}
|
|
|
|
private static func removeAllSessions() {
|
|
databaseStorage.write { transaction in
|
|
let signalProtoclStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
|
|
|
|
let signalProtocolStoreACI = signalProtoclStoreManager.signalProtocolStore(for: .aci)
|
|
signalProtocolStoreACI.sessionStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStoreACI.signedPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStoreACI.preKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStoreACI.kyberPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
|
|
let signalProtocolStorePNI = signalProtoclStoreManager.signalProtocolStore(for: .pni)
|
|
signalProtocolStorePNI.sessionStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStorePNI.signedPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStorePNI.preKeyStore.removeAll(tx: transaction.asV2Write)
|
|
signalProtocolStorePNI.kyberPreKeyStore.removeAll(tx: transaction.asV2Write)
|
|
}
|
|
}
|
|
|
|
private static func removeLocalPniIdentityKey() {
|
|
databaseStorage.write { transaction in
|
|
let identityManager = DependenciesBridge.shared.identityManager
|
|
identityManager.setIdentityKeyPair(nil, for: .pni, tx: transaction.asV2Write)
|
|
}
|
|
}
|
|
|
|
private static func logStickerSuggestions() {
|
|
var emojiSet = Set<String>()
|
|
databaseStorage.read { transaction in
|
|
StickerManager.installedStickerPacks(transaction: transaction).forEach { stickerPack in
|
|
stickerPack.items.forEach { item in
|
|
let emojiString = item.emojiString
|
|
if !emojiString.isEmpty {
|
|
Logger.verbose("emojiString: \(emojiString)")
|
|
emojiSet.insert(emojiString)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let combinedEmojiString = emojiSet.sorted().joined(separator: " ")
|
|
Logger.verbose("emoji: \(combinedEmojiString)")
|
|
}
|
|
|
|
private static func logLocalAccount() {
|
|
guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress else {
|
|
owsFailDebug("Missing localAddress.")
|
|
return
|
|
}
|
|
if let serviceId = localAddress.serviceId {
|
|
Logger.verbose("localAddress serviceId: \(serviceId)")
|
|
}
|
|
if let phoneNumber = localAddress.phoneNumber {
|
|
Logger.verbose("localAddress phoneNumber: \(phoneNumber)")
|
|
}
|
|
}
|
|
|
|
private static func logSignalRecipients() {
|
|
Self.databaseStorage.read { tx in
|
|
SignalRecipient.anyEnumerate(transaction: tx, batchingPreference: .batched(32)) { signalRecipient, _ in
|
|
Logger.verbose("SignalRecipient: \(signalRecipient.addressComponentsDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func clearProfileKeyCredentials() {
|
|
Self.databaseStorage.write { transaction in
|
|
Self.versionedProfiles.clearProfileKeyCredentials(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private static func clearLocalCustomEmoji() {
|
|
Self.databaseStorage.write { transaction in
|
|
ReactionManager.setCustomEmojiSet(nil, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private static func showFlagDatabaseAsCorruptedUi() {
|
|
OWSActionSheets.showConfirmationAlert(
|
|
title: "Are you sure?",
|
|
message: "This will flag your database as corrupted, which may mean all your data is lost. Are you sure you want to continue?",
|
|
proceedTitle: "Corrupt my database",
|
|
proceedStyle: .destructive
|
|
) { _ in
|
|
DatabaseCorruptionState.flagDatabaseAsCorrupted(
|
|
userDefaults: CurrentAppContext().appUserDefaults()
|
|
)
|
|
owsFail("Crashing due to (intentional) database corruption")
|
|
}
|
|
}
|
|
|
|
private static func showFlagDatabaseAsReadCorruptedUi() {
|
|
OWSActionSheets.showConfirmationAlert(
|
|
title: "Are you sure?",
|
|
message: "This will flag your database as possibly corrupted. It will not trigger a recovery on next startup. However, if you select the 'Make next app launch fail', the startup following the crash will funnel into the database recovery flow.",
|
|
proceedTitle: "Mark database corrupted on read",
|
|
proceedStyle: .destructive
|
|
) { _ in
|
|
DatabaseCorruptionState.flagDatabaseAsReadCorrupted(
|
|
userDefaults: CurrentAppContext().appUserDefaults()
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func clearMyStoryPrivacySettings() {
|
|
Self.databaseStorage.write { transaction in
|
|
guard let myStoryThread = TSPrivateStoryThread.getMyStory(transaction: transaction) else {
|
|
return
|
|
}
|
|
// Set to all connections which is the default.
|
|
myStoryThread.updateWithStoryViewMode(
|
|
.blockList,
|
|
addresses: [],
|
|
updateStorageService: false, /* storage service updated below */
|
|
transaction: transaction
|
|
)
|
|
StoryManager.setHasSetMyStoriesPrivacy(false, shouldUpdateStorageService: true, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private static func enableUsernameEducation() {
|
|
databaseStorage.write { tx in
|
|
DependenciesBridge.shared.usernameEducationManager.setShouldShowUsernameEducation(
|
|
true,
|
|
tx: tx.asV2Write
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func enableUsernameLinkTooltip() {
|
|
databaseStorage.write { tx in
|
|
DependenciesBridge.shared.usernameEducationManager.setShouldShowUsernameLinkTooltip(
|
|
true,
|
|
tx: tx.asV2Write
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func removeAllRecordedExperienceUpgrades() {
|
|
databaseStorage.write { transaction in
|
|
ExperienceUpgrade.anyRemoveAllWithInstantiation(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private static func showPinReminder() {
|
|
let viewController = PinReminderViewController()
|
|
UIApplication.shared.frontmostViewController!.present(viewController, animated: true)
|
|
}
|
|
|
|
private static func showSpoilerAnimationTestController() {
|
|
let viewController = SpoilerAnimationTestController()
|
|
UIApplication.shared.frontmostViewController!.present(viewController, animated: true)
|
|
}
|
|
}
|
|
|
|
#endif
|