// // HelperTools.m // Monal // // Created by Friedrich Altheide on 08.07.20. // Copyright © 2020 Monal.im. All rights reserved. // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #import #import #import #import #import #import #import #import //can not be imported, use extern declaration instead //#import extern int64_t kscrs_getNextCrashReport(char* crashReportPathBuffer); #import #import "hsluv.h" #import "HelperTools.h" #import "MLXMPPManager.h" #import "MLPubSub.h" #import "MLUDPLogger.h" #import "MLHandler.h" #import "MLBasePaser.h" #import "MLXMLNode.h" #import "XMPPStanza.h" #import "XMPPIQ.h" #import "XMPPPresence.h" #import "XMPPMessage.h" #import "XMPPDataForm.h" #import "xmpp.h" #import "MLNotificationQueue.h" #import "MLContact.h" #import "MLMessage.h" #import "MLFiletransfer.h" #import "DataLayer.h" #import "OmemoState.h" #import "MLUDPLogger.h" #import "MLStreamRedirect.h" #import "commithash.h" #import "MLContactSoftwareVersionInfo.h" #import "IPC.h" #import "MLDelayableTimer.h" #import "Quicksy_Country.h" @import UserNotifications; @import CoreImage; @import CoreImage.CIFilterBuiltins; @import UIKit; @import AVFoundation; @import UniformTypeIdentifiers; @import QuickLookThumbnailing; @interface KSCrash() @property(nonatomic,readwrite,retain) NSString* basePath; @end @interface MLDelayableTimer() -(void) invalidate; @end @interface NSUserDefaults (SerializeNSObject) -(id) swizzled_objectForKey:(NSString*) defaultName; -(void) swizzled_setObject:(id) value forKey:(NSString*) defaultName; @end static char* _crashBundleName = "UnifiedReport"; static NSString* _processID; static DDFileLogger* _fileLogger = nil; static char _origLogfilePath[1024] = ""; static char _logfilePath[1024] = ""; static char _origProfilePath[1024] = ""; static char _profilePath[1024] = ""; static NSObject* _isAppExtensionLock = nil; static NSMutableDictionary* _versionInfoCache; static MLStreamRedirect* _stdoutRedirector = nil; static MLStreamRedirect* _stderrRedirector = nil; static volatile void (*_oldExceptionHandler)(NSException*) = NULL; #if TARGET_OS_MACCATALYST static objc_exception_preprocessor _oldExceptionPreprocessor = NULL; #endif //shamelessly stolen from utils.ip in conversations source static NSRegularExpression* IPV4; static NSRegularExpression* IPV6_HEX4DECCOMPRESSED; static NSRegularExpression* IPV6_6HEX4DEC; static NSRegularExpression* IPV6_HEXCOMPRESSED; static NSRegularExpression* IPV6; //add own crash info (used by rust panic handler) //see https://alastairs-place.net/blog/2013/01/10/interesting-os-x-crash-report-tidbits/ //and kscrash sources (KSDynamicLinker.c) #pragma pack(8) static struct { unsigned version; const char* message; const char* signature; const char* backtrace; const char* message2; void* reserved; void* reserved2; void* reserved3; // First introduced in version 5 } _crash_info __attribute__((section("__DATA, __crash_info"))) = { 5, 0, 0, 0, 0, 0, 0, 0 }; #pragma pack() void exitLogging(void) { DDLogInfo(@"exit() was called..."); [HelperTools flushLogsWithTimeout:0.250]; return; } // see: https://developer.apple.com/library/archive/qa/qa1361/_index.html // Returns true if the current process is being debugged (either // running under the debugger or has a debugger attached post facto). bool isDebugerActive(void) { int junk; int mib[4]; struct kinfo_proc info; size_t size; // Initialize the flags so that, if sysctl fails for some bizarre // reason, we get a predictable result. info.kp_proc.p_flag = 0; // Initialize mib, which tells sysctl the info we want, in this case // we're looking for information about a specific process ID. mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = getpid(); // Call sysctl size = sizeof(info); junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); assert(junk == 0); // We're being debugged if the P_TRACED flag is set. return ( (info.kp_proc.p_flag & P_TRACED) != 0 ); } //see https://stackoverflow.com/a/2180788 int asyncSafeCopyFile(const char* from, const char* to) { int fd_to, fd_from; char buf[1024]; ssize_t nread; int saved_errno; fd_from = open(from, O_RDONLY); if (fd_from < 0) return -1; fd_to = open(to, O_WRONLY | O_CREAT | O_EXCL, 0660); if (fd_to < 0) goto out_error; while((nread = read(fd_from, buf, sizeof buf)) > 0) { char *out_ptr = buf; ssize_t nwritten; do { nwritten = write(fd_to, out_ptr, nread); if (nwritten >= 0) { nread -= nwritten; out_ptr += nwritten; } else if (errno != EINTR) { goto out_error; } } while (nread > 0); } if (nread == 0) { if (close(fd_to) < 0) { fd_to = -1; goto out_error; } close(fd_from); /* Success! */ return 0; } out_error: saved_errno = errno; close(fd_from); if (fd_to >= 0) close(fd_to); errno = saved_errno; return -1; } static void addFilePathWithSize(const KSCrashReportWriter* writer, char* name, char* filePath) { struct stat st; char name_size[64]; strncpy(name_size, name, 64); name_size[63] = '\0'; strncat(name_size, "_size", 64); name_size[63] = '\0'; writer->addStringElement(writer, name, filePath); stat(filePath, &st); writer->addIntegerElement(writer, name_size, st.st_size); } static void crash_callback(const KSCrashReportWriter* writer) { //copy current logfile int logfileCopyRetval = asyncSafeCopyFile(_origLogfilePath, _logfilePath); int errnoLogfileCopy = errno; writer->addStringElement(writer, "logfileCopied", "YES"); writer->addIntegerElement(writer, "logfileCopyResult", logfileCopyRetval); writer->addIntegerElement(writer, "logfileCopyErrno", errnoLogfileCopy); addFilePathWithSize(writer, "logfileCopy", _logfilePath); //this comes last to make sure we see size differences if the logfile got written during crash data collection (could be other processes) addFilePathWithSize(writer, "currentLogfile", _origLogfilePath); //copy current profiling file (see https://leodido.dev/demystifying-profraw/) int profileCopyRetval = asyncSafeCopyFile(_origProfilePath, _profilePath); int errnoProfileCopy = errno; writer->addStringElement(writer, "profileCopied", "YES"); writer->addIntegerElement(writer, "profileCopyResult", profileCopyRetval); writer->addIntegerElement(writer, "profileCopyErrno", errnoProfileCopy); addFilePathWithSize(writer, "profileCopy", _profilePath); //this comes last to make sure we see size differences if the logfile got written during crash data collection (could be other processes) addFilePathWithSize(writer, "currentProfile", _origProfilePath); } void logException(NSException* exception) { #if TARGET_OS_MACCATALYST NSString* prefix = @"POSSIBLE_CRASH"; #else NSString* prefix = @"CRASH"; #endif //log error and flush all logs [DDLog flushLog]; DDLogError(@"*****************\n%@(%@): %@\nUserInfo: %@\nStack Trace: %@", prefix, [exception name], [exception reason], [exception userInfo], [exception callStackSymbols]); [DDLog flushLog]; [HelperTools flushLogsWithTimeout:0.250]; } void uncaughtExceptionHandler(NSException* exception) { logException(exception); //don't report that crash through KSCrash if the debugger is active if(isDebugerActive()) { DDLogError(@"Not reporting crash through KSCrash: debugger is active!"); return; } //make sure this crash will be recorded by kscrash using the NSException rather than the c++ exception thrown by the objc runtime //this will make sure that the stacktrace matches the objc exception rather than being a top level c++ stacktrace KSCrash.sharedInstance.uncaughtExceptionHandler(exception); } //this function will only be in use under macos alpha builds to log every exception (even when catched with @try-@catch constructs) #if TARGET_OS_MACCATALYST static id preprocess(id exception) { id preprocessed = exception; if(_oldExceptionPreprocessor != NULL) preprocessed = _oldExceptionPreprocessor(exception); logException(preprocessed); return preprocessed; } #endif void swizzle(Class c, SEL orig, SEL new) { Method origMethod = class_getInstanceMethod(c, orig); Method newMethod = class_getInstanceMethod(c, new); if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); else method_exchangeImplementations(origMethod, newMethod); } static void notification_center_logging(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) { DDLogDebug(@"NSNotification %@ with %@: %@", name, object, userInfo); } @implementation WeakContainer -(id) initWithObj:(id) obj { self = [super init]; self.obj = obj; return self; } @end @implementation NSUserDefaults (SerializeNSObject) -(id) swizzled_objectForKey:(NSString*) defaultName { //this will call the original not this one, because of swizzling! id data = [self swizzled_objectForKey:defaultName]; //always unserialize this: every real NSData should be serialized to NSData (e.g. an NSData containing a serialized NSData) //and therefore any exception thrown by unserialize of not serialized data should never happen as it is an implementation error in Monal if([data isKindOfClass:[NSData class]]) { @try { return [HelperTools unserializeData:data]; } @catch (NSException* exception) { NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithDictionary:nilDefault(exception.userInfo, @{})]; [userInfo addEntriesFromDictionary:@{@"userDefaultsName":defaultName}]; @throw [NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo]; } } return data; } -(void) swizzled_setObject:(id) value forKey:(NSString*) defaultName { id toSave = value; //these are the default datatypes/class clusters already handled by NSUserDefaults //(NSData gets a special handling by us and is therefore not listed here) if( [value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSDate class]] || [value isKindOfClass:[NSURL class]] || [value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSMutableDictionary class]] || [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSMutableArray class]] || value == nil ) ; //do nothing, already handled by original NSUserDefaults method //every NSData should be double serialized (see swizzled_objectForKey: above for a detailed explanation) //everything else will just be (single) serialized to NSData else toSave = [HelperTools serializeObject:value]; return [self swizzled_setObject:toSave forKey:defaultName]; } //see https://stackoverflow.com/a/13326633 and https://fek.io/blog/method-swizzling-in-obj-c-and-swift/ +(void) load { if(self == NSUserDefaults.self) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ swizzle([self class], @selector(objectForKey:), @selector(swizzled_objectForKey:)); swizzle([self class], @selector(setObject:forKey:), @selector(swizzled_setObject:forKey:)); }); } } @end @implementation HelperTools +(void) initialize { _isAppExtensionLock = [NSObject new]; _versionInfoCache = [NSMutableDictionary new]; u_int32_t i = arc4random(); _processID = [self hexadecimalString:[NSData dataWithBytes:&i length:sizeof(i)]]; //shamelessly stolen from utils.ip in conversations source IPV4 = [NSRegularExpression regularExpressionWithPattern:@"\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; IPV6_HEX4DECCOMPRESSED = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; IPV6_6HEX4DEC = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z" options:0 error:nil]; IPV6_HEXCOMPRESSED = [NSRegularExpression regularExpressionWithPattern:@"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z" options:0 error:nil]; IPV6 = [NSRegularExpression regularExpressionWithPattern:@"\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z" options:0 error:nil]; } +(void) installExceptionHandler { //only install our exception handler if not yet installed _oldExceptionHandler = (volatile void (*)(NSException*))NSGetUncaughtExceptionHandler(); if((void*)_oldExceptionHandler != (void*)uncaughtExceptionHandler) { DDLogVerbose(@"Replaced unhandled exception handler, old handler: %p, new handler: %p", NSGetUncaughtExceptionHandler(), &uncaughtExceptionHandler); NSSetUncaughtExceptionHandler(uncaughtExceptionHandler); } #if TARGET_OS_MACCATALYST //this is needed for catalyst because catalyst apps are based on NSApplication which will swallow exceptions on the main thread and just continue //see: https://stackoverflow.com/questions/3336278/why-is-raising-an-nsexception-not-bringing-down-my-application //obj exception handling explanation: https://stackoverflow.com/a/28391007/3528174 //objc exception implementation: https://opensource.apple.com/source/objc4/objc4-818.2/runtime/objc-exception.mm.auto.html //objc exception header: https://opensource.apple.com/source/objc4/objc4-818.2/runtime/objc-exception.h.auto.html //example C++ exception ABI: https://github.com/nicolasbrailo/cpp_exception_handling_abi/tree/master/abi_v12 //this will log the exception if(_oldExceptionPreprocessor == NULL) _oldExceptionPreprocessor = objc_setExceptionPreprocessor(preprocess); //this will stop the swallowing [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"NSApplicationCrashOnExceptions": @YES}]; #endif } +(void) __attribute__((noreturn)) MLAssertWithText:(NSString*) text andUserData:(id) userInfo andFile:(const char* const) file andLine:(int) line andFunc:(const char* const) func { NSString* fileStr = [self sanitizeFilePath:file]; DDLogError(@"Assertion triggered at %@:%d in %s", fileStr, line, func); @throw [NSException exceptionWithName:[NSString stringWithFormat:@"MLAssert triggered at %@:%d in %s with reason '%@' and userInfo: %@", fileStr, line, func, text, userInfo] reason:text userInfo:userInfo]; } +(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBacktrace:(NSString*) backtrace { NSString* abort_msg = [NSString stringWithFormat:@"RUST_PANIC: %@", text]; //set crash_info_message in DATA section of our binary image //see https://alastairs-place.net/blog/2013/01/10/interesting-os-x-crash-report-tidbits/ _crash_info.message = abort_msg.UTF8String; _crash_info.signature = abort_msg.UTF8String; //use signature for apple crash reporter which does not handle message field _crash_info.backtrace = backtrace.UTF8String; //log error and flush all logs [DDLog flushLog]; DDLogError(@"*****************\n%@\n%@", abort_msg, backtrace); [DDLog flushLog]; [HelperTools flushLogsWithTimeout:0.250]; //now abort everything abort(); } +(void) __attribute__((noreturn)) throwExceptionWithName:(NSString*) name reason:(NSString*) reason userInfo:(NSDictionary* _Nullable) userInfo { @throw [NSException exceptionWithName:name reason:reason userInfo:userInfo]; } +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere andDisableAccount:(BOOL) disableAccount { [self postError:description withNode:node andAccount:account andIsSevere:isSevere]; //disconnect and reset state (including pipelined auth etc.) //this has to be done before disabling the account to not trigger an assertion [[MLXMPPManager sharedInstance] disconnectAccount:account.accountID withExplicitLogout:YES]; //make sure we don't try this again even when the mainapp/appex gets restarted NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:YES]; accountDic[kEnabled] = @NO; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; } +(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere { NSString* message = description; if(node) message = [HelperTools extractXMPPError:node withDescription:description]; DDLogError(@"Notifying user about %@ error: %@", isSevere ? @"SEVERE" : @"non-severe", message); [[MLNotificationQueue currentQueue] postNotificationName:kXMPPError object:account userInfo:@{@"message": message, @"isSevere":@(isSevere)}]; } +(void) showErrorOnAlpha:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp* _Nullable) account andFile:(char*) file andLine:(int) line andFunc:(char*) func { NSString* fileStr = [self sanitizeFilePath:file]; NSString* message = description; if(node) message = [self extractXMPPError:node withDescription:description]; #ifdef IS_ALPHA DDLogError(@"Notifying alpha user about error on account %@ at %@:%d in %s: %@", account, fileStr, line, func, message); if(account != nil) [[MLNotificationQueue currentQueue] postNotificationName:kXMPPError object:account userInfo:@{@"message": message, @"isSevere":@YES}]; else { UNMutableNotificationContent* content = [UNMutableNotificationContent new]; content.title = @"Global Error"; content.body = message; content.sound = [UNNotificationSound defaultSound]; UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString] content:content trigger:nil]; NSError* error = [self postUserNotificationRequest:request]; if(error) DDLogError(@"Error posting global alpha xmppError notification: %@", error); } #else DDLogWarn(@"Ignoring alpha-only error at %@:%d in %s: %@", fileStr, line, func, message); #endif } +(NSString*) extractXMPPError:(XMPPStanza*) stanza withDescription:(NSString*) description { if(description == nil || [description isEqualToString:@""]) description = @"XMPP Error"; NSMutableString* message = [description mutableCopy]; NSString* errorReason = [stanza findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}!text$"]; if(errorReason && ![errorReason isEqualToString:@""]) [message appendString:[NSString stringWithFormat:@": %@", errorReason]]; NSString* errorText = [stanza findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}text#"]; if(errorText && ![errorText isEqualToString:@""]) [message appendString:[NSString stringWithFormat:@" (%@)", errorText]]; return message; } +(void) initSystem { BOOL enableDefaultLogAndCrashFramework = YES; #if TARGET_OS_SIMULATOR // Automatically switch between the debug technique of TMolitor and FAltheide enableDefaultLogAndCrashFramework = [[HelperTools defaultsDB] boolForKey:@"udpLoggerEnabled"]; #endif if(enableDefaultLogAndCrashFramework) { [self configureLogging]; //don't install KSCrash if the debugger is active if(!isDebugerActive()) [self installCrashHandler]; else DDLogWarn(@"Not installing crash handler: debugger is active!"); [self installExceptionHandler]; } else [self configureXcodeLogging]; //see https://stackoverflow.com/a/3738387 CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), NULL, notification_center_logging, NULL, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); atexit(exitLogging); //set right path for llvm default.profraw file NSString* profrawFilePath = [[HelperTools getContainerURLForPathComponents:@[@"default.profraw"]] path]; setenv("LLVM_PROFILE_FILE", profrawFilePath.UTF8String, 1); [SwiftHelpers initSwiftHelpers]; [self activityLog]; } +(void) configureDefaultAudioSession { AVAudioSession* audioSession = [AVAudioSession sharedInstance]; NSError* error; DDLogDebug(@"configuring default audio session..."); AVAudioSessionCategoryOptions options = 0; options |= AVAudioSessionCategoryOptionMixWithOthers; //options |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers; //options |= AVAudioSessionCategoryOptionAllowBluetooth; //options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; //options |= AVAudioSessionCategoryOptionAllowAirPlay; [audioSession setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeDefault options:options error:&error]; if(error != nil) DDLogError(@"failed to configure audio session: %@", error); [audioSession setActive:YES withOptions:0 error:&error]; if(error != nil) DDLogError(@"error activating audio session: %@", error); DDLogVerbose(@"current audio route: %@", audioSession.currentRoute); } +(NSDictionary*) getInvalidPushServers { return @{ @"ios13push.monal.im": nilWrapper([[[UIDevice currentDevice] identifierForVendor] UUIDString]), @"push.monal.im": nilWrapper([[[UIDevice currentDevice] identifierForVendor] UUIDString]), @"us.prod.push.monal-im.org": nilWrapper(nil), }; } +(NSString*) getSelectedPushServerBasedOnLocale { #ifdef IS_ALPHA return @"alpha.push.monal-im.org"; #else return @"eu.prod.push.monal-im.org"; /* if([[[NSLocale currentLocale] countryCode] isEqualToString:@"US"]) { return @"us.prod.push.monal-im.org"; } else { return @"eu.prod.push.monal-im.org"; } */ #endif } +(NSDictionary*) getAvailablePushServers { return @{ //@"us.prod.push.monal-im.org": @"US", @"eu.prod.push.monal-im.org": @"Europe", @"alpha.push.monal-im.org": @"Alpha/Debug (more Logging)", #ifdef IS_ALPHA @"disabled.push.monal-im.org": @"Disabled - Alpha Test", #endif }; } +(NSArray*) getFailoverStunServers { return @[ #ifdef IS_ALPHA @"stuns:alpha.turn.monal-im.org:443", @"stuns:alpha.turn.monal-im.org:3478", #else @"stuns:eu.prod.turn.monal-im.org:443", @"stuns:eu.prod.turn.monal-im.org:3478", #endif ]; } //this wrapper is needed, because MLChatImageCell can't import our monalxmpp-Swift bridging header, but importing HelperTools is okay +(AnyPromise*) renderUIImageFromSVGURL:(NSURL* _Nullable) url { return [SwiftHelpers _renderUIImageFromSVGURL:url]; } //this wrapper is needed, because MLChatImageCell can't import our monalxmpp-Swift bridging header, but importing HelperTools is okay +(AnyPromise*) renderUIImageFromSVGData:(NSData* _Nullable) data { return [SwiftHelpers _renderUIImageFromSVGData:data]; } +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue { //apparently setting someQueue.suspended = YES does return before the queue is actually suspended //--> busy wait for someQueue.suspended == YES int busyWaitCounter = 0; NSTimeInterval waitTime = 0.0; NSDate* startTime = [NSDate date]; while([queue isSuspended] != YES) { busyWaitCounter++; waitTime = [[NSDate date] timeIntervalSinceDate:startTime]; MLAssert(waitTime <= 4.0, @"Busy wait for queue freeze took longer than 4.0 seconds!", (@{@"queue": queue, @"name": queue.name})); } if(busyWaitCounter > 0) DDLogWarn(@"busyWaitFor:%@ --> busyWaitCounter=%d, waitTime=%f", queue.name, busyWaitCounter, waitTime); } +(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier { switch(identifier) { case MLDefinedIdentifier_kAppGroup: return kAppGroup; break; case MLDefinedIdentifier_kMonalOpenURL: return kMonalOpenURL; break; case MLDefinedIdentifier_kBackgroundProcessingTask: return kBackgroundProcessingTask; break; case MLDefinedIdentifier_kBackgroundRefreshingTask: return kBackgroundRefreshingTask; break; case MLDefinedIdentifier_kMonalKeychainName: return kMonalKeychainName; break; case MLDefinedIdentifier_kMucTypeGroup: return kMucTypeGroup; break; case MLDefinedIdentifier_kMucRoleModerator: return kMucRoleModerator; break; case MLDefinedIdentifier_kMucRoleNone: return kMucRoleNone; break; case MLDefinedIdentifier_kMucRoleParticipant: return kMucRoleParticipant; break; case MLDefinedIdentifier_kMucRoleVisitor: return kMucRoleVisitor; break; case MLDefinedIdentifier_kMucAffiliationOwner: return kMucAffiliationOwner; break; case MLDefinedIdentifier_kMucAffiliationAdmin: return kMucAffiliationAdmin; break; case MLDefinedIdentifier_kMucAffiliationMember: return kMucAffiliationMember; break; case MLDefinedIdentifier_kMucAffiliationOutcast: return kMucAffiliationOutcast; break; case MLDefinedIdentifier_kMucAffiliationNone: return kMucAffiliationNone; break; case MLDefinedIdentifier_kMucActionShowProfile: return kMucActionShowProfile; break; case MLDefinedIdentifier_kMucActionReinvite: return kMucActionReinvite; break; case MLDefinedIdentifier_kMucTypeChannel: return kMucTypeChannel; break; case MLDefinedIdentifier_SHORT_PING: return @(SHORT_PING); break; case MLDefinedIdentifier_LONG_PING: return @(LONG_PING); break; case MLDefinedIdentifier_MUC_PING: return @(MUC_PING); break; case MLDefinedIdentifier_BGFETCH_DEFAULT_INTERVAL: return @(BGFETCH_DEFAULT_INTERVAL); break; default: unreachable(@"unknown MLDefinedIdentifier!"); } } +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier { static NSMutableDictionary* runloops = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ runloops = [NSMutableDictionary new]; }); //every identifier has its own thread priority/qos class __block dispatch_queue_priority_t priority; __block char* name; switch(identifier) { case MLRunLoopIdentifierNetwork: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; name = "im.monal.runloop.networking"; break; case MLRunLoopIdentifierTimer: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; name = "im.monal.runloop.timer"; break; default: unreachable(@"unknown runloop identifier!"); } @synchronized(runloops) { if(runloops[@(identifier)] == nil) { NSCondition* condition = [NSCondition new]; [condition lock]; dispatch_async(dispatch_queue_create_with_target(name, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(priority, 0)), ^{ //set thread name, too (not only runloop name) [NSThread.currentThread setName:[NSString stringWithFormat:@"%s", name]]; //we don't need an @synchronized block around this because the @synchronized block of the outer thread //waits until we signal our condition (e.g. no other thread can race with us) NSRunLoop* localLoop = runloops[@(identifier)] = [NSRunLoop currentRunLoop]; [condition lock]; [condition signal]; [condition unlock]; while(YES) { [localLoop run]; usleep(10000); //sleep 10ms if we ever return from our runloop to not consume too much cpu } }); [condition wait]; [condition unlock]; } return runloops[@(identifier)]; } } +(NSError* _Nullable) hardLinkOrCopyFile:(NSString*) from to:(NSString*) to { NSError* error = nil; NSFileManager* fileManager = [NSFileManager defaultManager]; DDLogVerbose(@"Trying to hardlink file '%@' to '%@'...", from, to); [fileManager linkItemAtPath:from toPath:to error:&error]; if(error) { DDLogWarn(@"Hardlinking failed, trying normal copy operation: %@", error); error = nil; [fileManager copyItemAtPath:from toPath:to error:&error]; if(error) { DDLogWarn(@"File copy failed, too: %@", error); return error; } } return nil; } +(NSString*) getQueueThreadLabelFor:(DDLogMessage*) logMessage { NSString* queueThreadLabel = logMessage.threadName; if(![queueThreadLabel length]) queueThreadLabel = logMessage.queueLabel; if([@"com.apple.main-thread" isEqualToString:queueThreadLabel]) queueThreadLabel = @"main"; if(![queueThreadLabel length]) queueThreadLabel = logMessage.threadID; //remove already appended " (QOS: XXX)" because we want to append the QOS part ourselves NSRange range = [queueThreadLabel rangeOfString:@" (QOS: "]; if(range.length > 0) queueThreadLabel = [queueThreadLabel substringWithRange:NSMakeRange(0, range.location)]; return queueThreadLabel; } +(NSURL*) getFailoverTurnApiServer { NSString* turnApiServer; #ifdef IS_ALPHA turnApiServer = @"https://alpha.turn.monal-im.org"; #else turnApiServer = @"https://eu.prod.turn.monal-im.org"; #endif return [NSURL URLWithString:turnApiServer]; } +(BOOL) shouldProvideVoip { BOOL shouldProvideVoip = NO; #if TARGET_OS_MACCATALYST #ifdef IS_ALPHA shouldProvideVoip = YES; #endif #else #ifdef IS_QUICKSY NSLocale* userLocale = [NSLocale currentLocale]; shouldProvideVoip = !([userLocale.countryCode containsString: @"CN"] || [userLocale.countryCode containsString: @"CHN"]); #else shouldProvideVoip = YES; #endif #endif return shouldProvideVoip; } +(BOOL) isSandboxAPNS { #if TARGET_OS_SIMULATOR DDLogVerbose(@"APNS environment is: sandbox"); return YES; #else // check if were are sandbox or production NSString* embeddedProvPath; #if TARGET_OS_MACCATALYST NSString* bundleURL = [[NSBundle mainBundle] bundleURL].absoluteString; embeddedProvPath = [[[bundleURL componentsSeparatedByString:@"file://"] objectAtIndex:1] stringByAppendingString:@"Contents/embedded.provisionprofile"]; #else embeddedProvPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]; #endif DDLogVerbose(@"Loading embedded provision plist at: %@", embeddedProvPath); NSError* loadingError; NSString* embeddedProvStr = [NSString stringWithContentsOfFile:embeddedProvPath encoding:NSISOLatin1StringEncoding error:&loadingError]; if(embeddedProvStr == nil) { // fallback to production DDLogWarn(@"Could not read embedded provision (should be production install): %@", loadingError); DDLogVerbose(@"APNS environment is: production"); return NO; } NSScanner* plistScanner = [NSScanner scannerWithString:embeddedProvStr]; [plistScanner scanUpToString:@"" intoString:&plistStr]; plistStr = [NSString stringWithFormat:@"%@", plistStr]; DDLogVerbose(@"Extracted bundle plist string: %@", plistStr); NSError* plistError; NSPropertyListFormat format; NSDictionary* plist = [NSPropertyListSerialization propertyListWithData:[plistStr dataUsingEncoding:NSISOLatin1StringEncoding] options:NSPropertyListImmutable format:&format error:&plistError]; DDLogVerbose(@"Parsed plist: %@", plist); if(plistError != nil) { // fallback to production DDLogWarn(@"Could not parse embedded provision as plist: %@", plistError); DDLogVerbose(@"APNS environment is: production"); return NO; } if(plist[@"com.apple.developer.aps-environment"] && [@"production" isEqualToString:plist[@"com.apple.developer.aps-environment"]] == NO) { // sandbox DDLogWarn(@"aps-environmnet is set to: %@", plist[@"com.apple.developer.aps-environment"]); DDLogVerbose(@"APNS environment is: sandbox"); return YES; } if(plist[@"Entitlements"] && [@"production" isEqualToString:plist[@"Entitlements"][@"aps-environment"]] == NO) { // sandbox DDLogWarn(@"aps-environmnet is set to: %@", plist[@"Entitlements"][@"aps-environment"]); DDLogVerbose(@"APNS environment is: sandbox"); return YES; } // production DDLogVerbose(@"APNS environment is: production"); return NO; #endif } +(int) compareIOcted:(NSData*) data1 with:(NSData*) data2 { int result = memcmp(data1.bytes, data2.bytes, min(data1.length, data2.length)); if(result == 0 && data1.length < data2.length) return -1; else if(result == 0 && data1.length > data2.length) return 1; return result; } +(NSURL*) getContainerURLForPathComponents:(NSArray*) components { static NSURL* containerUrl; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ containerUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kAppGroup]; }); MLAssert(containerUrl != nil, @"Container URL should never be nil!"); NSURL* retval = containerUrl; for(NSString* component in components) retval = [retval URLByAppendingPathComponent:component]; return retval; } +(NSURL*) getSharedDocumentsURLForPathComponents:(NSArray*) components { NSURL* sharedUrl = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; for(NSString* component in components) sharedUrl = [sharedUrl URLByAppendingPathComponent:component]; NSURLComponents* urlComponents = [NSURLComponents componentsWithURL:sharedUrl resolvingAgainstBaseURL:NO]; urlComponents.scheme = @"shareddocuments"; return urlComponents.URL; } +(NSData*) serializeObject:(id) obj { NSError* error; NSData* data = [NSKeyedArchiver archivedDataWithRootObject:obj requiringSecureCoding:YES error:&error]; if(error) @throw [NSException exceptionWithName:@"NSError" reason:[NSString stringWithFormat:@"%@", error] userInfo:@{@"error": error}]; return data; } +(id) unserializeData:(NSData*) data { NSError* error; id obj = [NSKeyedUnarchiver unarchivedObjectOfClasses:[[NSSet alloc] initWithArray:@[ [NSData class], [NSMutableData class], [NSMutableDictionary class], [NSDictionary class], [NSMutableSet class], [NSSet class], [NSMutableArray class], [NSArray class], [NSNumber class], [NSString class], [NSDate class], [MLHandler class], [MLXMLNode class], [XMPPIQ class], [XMPPPresence class], [XMPPMessage class], [XMPPDataForm class], [MLContact class], [MLMessage class], [NSURL class], [OmemoState class], [MLContactSoftwareVersionInfo class], [Quicksy_Country class], ]] fromData:data error:&error]; if(error) @throw [NSException exceptionWithName:@"NSError" reason:[NSString stringWithFormat:@"%@", error] userInfo:@{@"error": error}]; return obj; } +(NSError* _Nullable) postUserNotificationRequest:(UNNotificationRequest*) request { __block NSError* retval = nil; NSCondition* condition = [NSCondition new]; [condition lock]; monal_void_block_t cancelTimeout = createTimer(1.0, (^{ DDLogError(@"Waiting for notification center took more than 1.0 second, continuing anyways"); [condition lock]; [condition signal]; [condition unlock]; })); [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError* _Nullable error) { if(error) DDLogError(@"Error posting notification: %@", error); retval = error; [condition lock]; [condition signal]; [condition unlock]; }]; [condition wait]; [condition unlock]; cancelTimeout(); return retval; } +(void) createAVURLAssetFromFile:(NSString*) file havingMimeType:(NSString*) mimeType andFileExtension:(NSString* _Nullable) fileExtension withCompletionHandler:(void(^)(AVURLAsset* _Nullable)) completion { NSURL* fileUrl = [NSURL fileURLWithPath:file]; if(@available(iOS 17.0, macCatalyst 17.0, *)) { //generate an AVURLAsset using the modern ios 17 method to attach a mime type to an AVURLAsset return completion([AVURLAsset URLAssetWithURL:fileUrl options:@{AVURLAssetOverrideMIMETypeKey: mimeType}]); } //TODO: instead of this symlink method hack, we *maybe* could use the AVURLAssetOutOfBandMIMETypeKey in place of //TODO: AVURLAssetOverrideMIMETypeKey on ios 16, BUT: that symbol isn't public and may be catched by apple review //TODO: (but it makes our code way cleaner than using this symlink stuff) DDLogDebug(@"Generating thumbnail with symlink method..."); if(fileExtension == nil) { //this will return nil if the mime type isn't known by apple fileExtension = [[UTType typeWithMIMEType:mimeType] preferredFilenameExtension]; //--> bail out if this is still nil if(fileExtension == nil) { DDLogWarn(@"Could not get file extension for file, not creating AVURLAsset..."); return completion(nil); } } NSURL* symlinkUrl = [self getContainerURLForPathComponents:@[ @"documentCache", [NSString stringWithFormat:@"tmp.avurlasset_symlink.%@.%@", fileUrl.lastPathComponent, fileExtension] ]]; NSError* error = nil; if([[NSFileManager defaultManager] fileExistsAtPath:symlinkUrl.path]) [[NSFileManager defaultManager] removeItemAtURL:symlinkUrl error:&error]; if(error != nil) { DDLogError(@"Could not delete old leftover symlink file at '%@': %@", symlinkUrl, error); return completion(nil); } [[NSFileManager defaultManager] createSymbolicLinkAtURL:symlinkUrl withDestinationURL:fileUrl error:&error]; if(error != nil) { DDLogError(@"Could not create symlink file '%@' pointing to '%@': %@", symlinkUrl, fileUrl, error); return completion(nil); } //create the AVURLAsset and invoke the callback using it completion([AVURLAsset URLAssetWithURL:fileUrl options:@{}]); //remove file afterwards and just log errors if removal of symlink fails [[NSFileManager defaultManager] removeItemAtURL:symlinkUrl error:&error]; if(error != nil) DDLogError(@"Could not clean up symlink file '%@' pointing to '%@': %@", symlinkUrl, fileUrl, error); } +(AnyPromise*) generateVideoThumbnailFromFile:(NSString*) file havingMimeType:(NSString*) mimeType andFileExtension:(NSString* _Nullable) fileExtension { return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { [self createAVURLAssetFromFile:file havingMimeType:mimeType andFileExtension:fileExtension withCompletionHandler:^(AVURLAsset* asset) { if(asset == nil) return resolve([NSError errorWithDomain:@"Monal" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Could not create AVURLAsset"}]); AVAssetImageGenerator* imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; imageGenerator.appliesPreferredTrackTransform=TRUE; CMTime time = CMTimeMakeWithSeconds(1, 600); [imageGenerator generateCGImageAsynchronouslyForTime:time completionHandler:^(CGImageRef image, CMTime actualTime, NSError* error) { if(error != nil) { DDLogError(@"Error generating thumbnail: %@", error); return resolve(error); } return resolve([UIImage imageWithCGImage:image]); }]; }]; }]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcompletion-handler" +(void) addUploadItemPreviewForItem:(NSURL* _Nullable) url provider:(NSItemProvider* _Nullable) provider andPayload:(NSMutableDictionary*) payload withCompletionHandler:(void(^)(NSMutableDictionary* _Nullable)) completion { void (^useProvider)() = ^() { if(provider == nil) { DDLogWarn(@"Can not creating preview image via item provider, no provider present: using generic doc image instead"); payload[@"preview"] = [UIImage systemImageNamed:@"doc"]; [url stopAccessingSecurityScopedResource]; return completion(payload); } else [provider loadPreviewImageWithOptions:nil completionHandler:^(UIImage* _Nullable previewImage, NSError* _Null_unspecified error) { if(error != nil || previewImage == nil) { if(url == nil) { DDLogWarn(@"Error creating preview image via item provider, using generic doc image instead: %@", error); payload[@"preview"] = [UIImage systemImageNamed:@"doc"]; } } else { DDLogVerbose(@"Managed to generate thumbnail for url=%@ using loadPreviewImageWithOptions: %@", url, previewImage); payload[@"preview"] = previewImage; } [url stopAccessingSecurityScopedResource]; return completion(payload); }]; }; if(url != nil) { DDLogVerbose(@"Generating thumbnail for url=%@", url); QLThumbnailGenerationRequest* request = [[QLThumbnailGenerationRequest alloc] initWithFileAtURL:url size:CGSizeMake(64, 64) scale:1.0 representationTypes:QLThumbnailGenerationRequestRepresentationTypeThumbnail]; NSURL* tmpURL = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory: YES]; tmpURL = [tmpURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; [QLThumbnailGenerator.sharedGenerator saveBestRepresentationForRequest:request toFileAtURL:tmpURL withContentType:UTTypePNG.identifier completionHandler:^(NSError *error) { if(error == nil) { UIImage* result = [UIImage imageWithContentsOfFile:[url path]]; [[NSFileManager defaultManager] removeItemAtURL:tmpURL error:nil]; //remove temporary file, we don't need it anymore if(result != nil) { payload[@"preview"] = result; DDLogVerbose(@"Managed to generate thumbnail for url=%@ using QLThumbnailGenerator: %@", url, result); [url stopAccessingSecurityScopedResource]; return completion(payload); //don't fall through on success } } //if we fall through to this point, either the thumbnail generation or the imageWithContentsOfFile above failed //--> try something else DDLogVerbose(@"Extracting thumbnail using imageWithContentsOfFile failed, retrying with imageWithContentsOfFile: %@", error); UIImage* result = [UIImage imageWithContentsOfFile:[url path]]; if(result != nil) { payload[@"preview"] = result; DDLogVerbose(@"Managed to generate thumbnail for url=%@ using imageWithContentsOfFile: %@", url, result); [url stopAccessingSecurityScopedResource]; return completion(payload); } else { DDLogVerbose(@"Thumbnail generation not successful - reverting to generic image for file: %@", error); UIDocumentInteractionController* imgCtrl = [UIDocumentInteractionController interactionControllerWithURL:url]; if(imgCtrl != nil && imgCtrl.icons.count > 0) { payload[@"preview"] = imgCtrl.icons.firstObject; DDLogVerbose(@"Managed to generate thumbnail for url=%@ using generic image for file: %@", url, imgCtrl.icons.firstObject); [url stopAccessingSecurityScopedResource]; return completion(payload); } } //try to generate video thumbnail [self generateVideoThumbnailFromFile:url.path havingMimeType:[UTType typeWithFilenameExtension:url.pathExtension].preferredMIMEType andFileExtension:url.pathExtension].then(^(UIImage* image) { payload[@"preview"] = image; DDLogVerbose(@"Managed to generate thumbnail for url=%@ using generateVideoThumbnailFromFile: %@", url, image); [url stopAccessingSecurityScopedResource]; return completion(payload); }).catch(^(NSError* error) { DDLogError(@"Could not create video thumbnail, using provider as last resort: %@", error); //last resort useProvider(); }); }]; } else useProvider(); } #pragma clang diagnostic pop #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcompletion-handler" +(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandler:(void(^)(NSMutableDictionary* _Nullable)) completion { NSMutableDictionary* payload = [NSMutableDictionary new]; //for a list of types, see UTCoreTypes.h in MobileCoreServices framework DDLogInfo(@"ShareProvider: %@", provider.registeredTypeIdentifiers); if(provider.suggestedName != nil) payload[@"filename"] = provider.suggestedName; void (^prepareFile)(NSURL*) = ^(NSURL* item) { NSError* error; [item startAccessingSecurityScopedResource]; [[NSFileCoordinator new] coordinateReadingItemAtURL:item options:NSFileCoordinatorReadingForUploading error:&error byAccessor:^(NSURL* _Nonnull newURL) { DDLogDebug(@"NSFileCoordinator called accessor: %@", newURL); payload[@"data"] = [MLFiletransfer prepareFileUpload:newURL]; //we can not use newURL here, because it will fall out of scope while the preview is rendered in another thread return [HelperTools addUploadItemPreviewForItem:item provider:provider andPayload:payload withCompletionHandler:completion]; }]; if(error != nil) { DDLogError(@"Error preparing file coordinator: %@", error); payload[@"error"] = error; [item stopAccessingSecurityScopedResource]; return completion(payload); } }; if([provider hasItemConformingToTypeIdentifier:@"com.apple.mapkit.map-item"]) { // convert map item to geo: [provider loadItemForTypeIdentifier:@"com.apple.mapkit.map-item" options:nil completionHandler:^(NSData* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } NSError* err; MKMapItem* mapItem = [NSKeyedUnarchiver unarchivedObjectOfClass:[MKMapItem class] fromData:item error:&err]; if(err != nil || mapItem == nil) { DDLogError(@"Error extracting mapkit item: %@", err); payload[@"error"] = err; return completion(payload); } else { DDLogInfo(@"Got mapkit item: %@", item); payload[@"type"] = @"geo"; payload[@"data"] = [NSString stringWithFormat:@"geo:%f,%f", mapItem.placemark.coordinate.latitude, mapItem.placemark.coordinate.longitude]; return [HelperTools addUploadItemPreviewForItem:nil provider:provider andPayload:payload withCompletionHandler:completion]; } }]; } //the apple-private autoloop gif type has a bug that does not allow to load this as normal gif --> try audiovisual content below else if([provider hasItemConformingToTypeIdentifier:UTTypeGIF.identifier] && ![provider hasItemConformingToTypeIdentifier:@"com.apple.private.auto-loop-gif"]) { /* [provider loadDataRepresentationForTypeIdentifier:UTTypeGIF.identifier completionHandler:^(NSData* data, NSError* error) { if(error != nil || data == nil) { DDLogError(@"Error extracting gif image from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got gif image data: %@", data); payload[@"type"] = @"file"; payload[@"data"] = [MLFiletransfer prepareDataUpload:data withFileExtension:@"gif"]; return [HelperTools addUploadItemPreviewForItem:nil provider:provider andPayload:payload withCompletionHandler:completion]; }]; */ [provider loadInPlaceFileRepresentationForTypeIdentifier:UTTypeGIF.identifier completionHandler:^(NSURL* _Nullable item, BOOL isInPlace, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting gif image from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got %@ gif image item: %@", isInPlace ? @"(in place)" : @"(copied)", item); payload[@"type"] = @"file"; return prepareFile(item); }]; } else if([provider hasItemConformingToTypeIdentifier:UTTypeAudiovisualContent.identifier]) { [provider loadItemForTypeIdentifier:UTTypeAudiovisualContent.identifier options:nil completionHandler:^(NSURL* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got audiovisual item: %@", item); payload[@"type"] = @"audiovisual"; return prepareFile(item); }]; } else if([provider hasItemConformingToTypeIdentifier:UTTypeImage.identifier]) { [provider loadItemForTypeIdentifier:UTTypeImage.identifier options:nil completionHandler:^(NSURL* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { //for example: image shared directly from screenshots DDLogWarn(@"Got error, retrying with UIImage: %@", error); [provider loadItemForTypeIdentifier:UTTypeImage.identifier options:nil completionHandler:^(UIImage* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got memory image item: %@", item); payload[@"type"] = @"image"; if(![[HelperTools defaultsDB] boolForKey:@"uploadImagesOriginal"]) { //use prepareUIImageUpload to resize the image to the configured quality payload[@"data"] = [MLFiletransfer prepareUIImageUpload:item]; } else payload[@"data"] = [MLFiletransfer prepareDataUpload:UIImagePNGRepresentation(item) withFileExtension:@"png"]; payload[@"preview"] = item; return completion(payload); }]; } else { DDLogInfo(@"Got image item: %@", item); payload[@"type"] = @"image"; if(![[HelperTools defaultsDB] boolForKey:@"uploadImagesOriginal"]) { [item startAccessingSecurityScopedResource]; [[NSFileCoordinator new] coordinateReadingItemAtURL:item options:NSFileCoordinatorReadingForUploading error:&error byAccessor:^(NSURL* _Nonnull newURL) { DDLogDebug(@"NSFileCoordinator called accessor for image: %@", newURL); UIImage* image = [UIImage imageWithContentsOfFile:[newURL path]]; DDLogDebug(@"Created UIImage: %@", image); //use prepareUIImageUpload to resize the image to the configured quality (instead of just uploading the raw image file) payload[@"data"] = [MLFiletransfer prepareUIImageUpload:image]; //we can not use newURL here, because it will fall out of scope while the preview is rendered in another thread return [HelperTools addUploadItemPreviewForItem:item provider:provider andPayload:payload withCompletionHandler:completion]; }]; } else return prepareFile(item); if(error != nil) { DDLogError(@"Error preparing file coordinator: %@", error); payload[@"error"] = error; [item stopAccessingSecurityScopedResource]; return completion(payload); } } }]; } /*else if([provider hasItemConformingToTypeIdentifier:(NSString*)]) { } else if([provider hasItemConformingToTypeIdentifier:(NSString*)]) { }*/ else if([provider hasItemConformingToTypeIdentifier:UTTypeContact.identifier]) { [provider loadItemForTypeIdentifier:UTTypeContact.identifier options:nil completionHandler:^(NSURL* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got contact item: %@", item); payload[@"type"] = @"contact"; return prepareFile(item); }]; } else if([provider hasItemConformingToTypeIdentifier:UTTypeFileURL.identifier]) { [provider loadItemForTypeIdentifier:UTTypeFileURL.identifier options:nil completionHandler:^(NSURL* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got file url item: %@", item); payload[@"type"] = @"file"; return prepareFile(item); }]; } else if([provider hasItemConformingToTypeIdentifier:(NSString*)@"com.apple.finder.node"]) { [provider loadItemForTypeIdentifier:UTTypeItem.identifier options:nil completionHandler:^(id item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } if([(NSObject*)item isKindOfClass:[NSURL class]]) { DDLogInfo(@"Got finder file url item: %@", item); payload[@"type"] = @"file"; return prepareFile((NSURL*)item); } else { DDLogError(@"Could not extract finder item"); payload[@"error"] = NSLocalizedString(@"Could not access Finder item!", @""); return completion(payload); } }]; } else if([provider hasItemConformingToTypeIdentifier:UTTypeURL.identifier]) { [provider loadItemForTypeIdentifier:UTTypeURL.identifier options:nil completionHandler:^(NSURL* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got internet url item: %@", item); payload[@"type"] = @"url"; payload[@"data"] = item.absoluteString; return [HelperTools addUploadItemPreviewForItem:nil provider:provider andPayload:payload withCompletionHandler:completion]; }]; } else if([provider hasItemConformingToTypeIdentifier:UTTypePlainText.identifier]) { [provider loadItemForTypeIdentifier:UTTypePlainText.identifier options:nil completionHandler:^(NSString* _Nullable item, NSError* _Null_unspecified error) { if(error != nil || item == nil) { DDLogError(@"Error extracting item from NSItemProvider: %@", error); payload[@"error"] = error; return completion(payload); } DDLogInfo(@"Got direct text item: %@", item); payload[@"type"] = @"text"; payload[@"data"] = item; return [HelperTools addUploadItemPreviewForItem:nil provider:provider andPayload:payload withCompletionHandler:completion]; }]; } else return completion(nil); } #pragma clang diagnostic pop //see https://gist.github.com/giaesp/7704753 +(UIImage* _Nullable) rotateImage:(UIImage* _Nullable) image byRadians:(CGFloat) rotation { if(image == nil) return nil; //Calculate Destination Size CGAffineTransform t = CGAffineTransformMakeRotation(rotation); CGRect sizeRect = (CGRect) {.size = image.size}; CGRect destRect = CGRectApplyAffineTransform(sizeRect, t); return [[[UIGraphicsImageRenderer alloc] initWithSize:destRect.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { CGContextRef context = rendererContext.CGContext; //Move the origin to the middle of the image to apply the transformation CGContextTranslateCTM(context, destRect.size.width / 2.0f, destRect.size.height / 2.0f); CGContextRotateCTM(context, rotation); //Draw the original image into the transformed context [image drawInRect:CGRectMake(-image.size.width / 2.0f, -image.size.height / 2.0f, image.size.width, image.size.height)]; }]; } +(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image { if(image == nil) return nil; return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { CGContextRef context = rendererContext.CGContext; //Move the origin to the middle of the image to apply the transformation CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); //Apply the y-axis mirroring transform CGContextScaleCTM(context, 1.0, -1.0); //Move the origin back to the bottom left corner CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); //Draw the original image into the transformed context [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; }]; } +(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image { if(image == nil) return nil; return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { CGContextRef context = rendererContext.CGContext; //Move the origin to the middle of the image to apply the transformation CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); //Apply the y-axis mirroring transform CGContextScaleCTM(context, -1.0, 1.0); //Move the origin back to the bottom left corner CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); //Draw the original image into the transformed context [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; }]; } +(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image { UIImage* badge = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:UIColor.redColor]; CGRect imgSize = CGRectMake(0, 0, image.size.width, image.size.height); CGRect dotSize = CGRectMake(image.size.width - 7, 0, 7, 7); return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { [image drawInRect:imgSize]; [badge drawInRect:dotSize blendMode:kCGBlendModeNormal alpha:1.0]; }]; } +(UIImageView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler { UIImageView* result; if(hasNotification) result = [[UIImageView alloc] initWithImage:[self imageWithNotificationBadgeForImage:image]]; else result = [[UIImageView alloc] initWithImage: image]; [result addGestureRecognizer:handler]; return result; } +(NSData*) resizeAvatarImage:(UIImage* _Nullable) image withCircularMask:(BOOL) circularMask toMaxBase64Size:(unsigned long) length { if(!image) return [NSData new]; int destinationSize = 480; int epsilon = 8; UIImage* clippedImage = image; UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat new]; format.opaque = NO; format.preferredRange = UIGraphicsImageRendererFormatRangeStandard; format.scale = 1.0; if(ABS(image.size.width - image.size.height) > epsilon) { //see this for different resizing techniques, memory consumption and other caveats: // - https://nshipster.com/image-resizing/ // - https://www.advancedswift.com/crop-image/ // - https://www.swiftjectivec.com/optimizing-images/ CGFloat minSize = MIN(image.size.width, image.size.height); CGRect drawImageRect = CGRectMake( (image.size.width - minSize) / -2.0, (image.size.height - minSize) / -2.0, image.size.width, image.size.height ); CGRect drawRect = CGRectMake( 0, 0, minSize, minSize ); DDLogInfo(@"Clipping avatar image %@ to %lux%lu pixels", image, (unsigned long)drawImageRect.size.width, (unsigned long)drawImageRect.size.height); DDLogDebug(@"minSize: %.2f, drawImageRect: (%.2f, %.2f, %.2f, %.2f)", minSize, drawImageRect.origin.x, drawImageRect.origin.y, drawImageRect.size.width, drawImageRect.size.height ); clippedImage = [[[UIGraphicsImageRenderer alloc] initWithSize:drawRect.size format:format] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull context __unused) { //not needed here, already done below //if(circularMask) // [[UIBezierPath bezierPathWithOvalInRect:drawRect] addClip]; [image drawInRect:drawImageRect]; }]; image = nil; //make sure we free our memory as soon as possible DDLogInfo(@"Clipped image is now: %@", clippedImage); } //shrink image to a maximum of 480x480 pixel (AVMakeRectWithAspectRatioInsideRect() keeps the aspect ratio) //CGRect dimensions = AVMakeRectWithAspectRatioInsideRect(image.size, CGRectMake(0, 0, 480, 480)); CGRect dimensions; if(clippedImage.size.width > destinationSize + epsilon) { dimensions = CGRectMake(0, 0, destinationSize, destinationSize); DDLogInfo(@"Now shrinking image to %lux%lu pixels", (unsigned long)dimensions.size.width, (unsigned long)dimensions.size.height); } else if(circularMask) { dimensions = CGRectMake(0, 0, clippedImage.size.width, clippedImage.size.height); DDLogInfo(@"Only masking image to a %lux%lu pixels circle", (unsigned long)dimensions.size.width, (unsigned long)dimensions.size.height); } else { dimensions = CGRectMake(0, 0, 0, 0); DDLogInfo(@"Not doing anything to image, everything is already perfect: %@", clippedImage); } //only shink/mask image if needed and requested (indicated by a dimension size > 0 UIImage* resizedImage = clippedImage; if(dimensions.size.width > 0) { resizedImage = [[[UIGraphicsImageRenderer alloc] initWithSize:dimensions.size format:format] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull context __unused) { if(circularMask) [[UIBezierPath bezierPathWithOvalInRect:dimensions] addClip]; [clippedImage drawInRect:dimensions]; }]; DDLogInfo(@"Shrinked/masked image is now: %@", resizedImage); } clippedImage = nil; //make sure we free our memory as soon as possible //masked images MUST be of type png because jpeg does no carry any transparency information NSData* data = nil; if(circularMask) { data = UIImagePNGRepresentation(resizedImage); DDLogInfo(@"Returning new avatar png data with size %lu for image: %@", (unsigned long)data.length, resizedImage); } else { //now reduce quality until image data is smaller than provided size unsigned int i = 0; double qualityList[] = {0.96, 0.80, 0.64, 0.48, 0.32, 0.24, 0.16, 0.10, 0.09, 0.08, 0.07, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01}; for(i = 0; (data == nil || (data.length * 1.5) > length) && i < sizeof(qualityList) / sizeof(qualityList[0]); i++) { DDLogDebug(@"Resizing new avatar to quality %f", qualityList[i]); data = UIImageJPEGRepresentation(resizedImage, qualityList[i]); DDLogDebug(@"New avatar size after changing quality: %lu", (unsigned long)data.length); } DDLogInfo(@"Returning new avatar jpeg data with size %lu and quality %f for image: %@", (unsigned long)data.length, qualityList[i-1], resizedImage); } return data; } +(double) report_memory { struct task_basic_info info; mach_msg_type_number_t size = TASK_BASIC_INFO_COUNT; kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size); if(kerr == KERN_SUCCESS) return ((CGFloat)info.resident_size / 1048576); else DDLogDebug(@"Error with task_info(): %s", mach_error_string(kerr)); return 1.0; //dummy value } +(UIColor*) generateColorFromJid:(NSString*) jid { //cache generated colors static NSMutableDictionary* cache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [NSMutableDictionary new]; }); if(cache[jid] != nil) return cache[jid]; //XEP-0392 implementation NSData* hash = [self sha1:[jid dataUsingEncoding:NSUTF8StringEncoding]]; uint16_t rawHue = CFSwapInt16LittleToHost(*(uint16_t*)[hash bytes]); double hue = (rawHue / 65536.0) * 360.0; double saturation = 100.0; double lightness = 50.0; double r, g, b; hsluv2rgb(hue, saturation, lightness, &r, &g, &b); return cache[jid] = [UIColor colorWithRed:r green:g blue:b alpha:1]; } +(NSString*) bytesToHuman:(int64_t) bytes { NSArray* suffixes = @[@"B", @"KiB", @"MiB", @"GiB", @"TiB", @"PiB", @"EiB"]; NSString* prefix = @""; double size = bytes; if(size < 0) { prefix = @"-"; size *= -1; } for(NSString* suffix in suffixes) if(size < 1024) return [NSString stringWithFormat:@"%@%.1F %@", prefix, size, suffix]; else size /= 1024.0; return [NSString stringWithFormat:@"%lld B", bytes]; } +(NSString*) stringFromToken:(NSData*) tokenIn { unsigned char* tokenBytes = (unsigned char*)[tokenIn bytes]; NSMutableString* token = [NSMutableString new]; NSUInteger counter = 0; while(counter < tokenIn.length) { [token appendString:[NSString stringWithFormat:@"%02x", (unsigned char)tokenBytes[counter]]]; counter++; } return token; } //proxy to not have full IPC class accessible from UI +(NSString* _Nullable) exportIPCDatabase { return [[IPC sharedInstance] exportDB]; } +(void) configureFileProtection:(NSString*) protectionLevel forFile:(NSString*) file { #if TARGET_OS_IPHONE NSFileManager* fileManager = [NSFileManager defaultManager]; if([fileManager fileExistsAtPath:file]) { //DDLogVerbose(@"protecting file '%@'...", file); NSError* error; [fileManager setAttributes:@{NSFileProtectionKey: protectionLevel} ofItemAtPath:file error:&error]; if(error) { DDLogError(@"Error configuring file protection level for: %@", file); @throw [NSException exceptionWithName:@"NSError" reason:[NSString stringWithFormat:@"%@", error] userInfo:@{@"error": error}]; } else ;//DDLogVerbose(@"file '%@' now protected", file); } else ;//DDLogVerbose(@"file '%@' does not exist!", file); #endif } +(void) configureFileProtectionFor:(NSString*) file { [self configureFileProtection:NSFileProtectionCompleteUntilFirstUserAuthentication forFile:file]; } +(NSDictionary*) splitJid:(NSString*) jid { //cache results static NSCache* cache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [NSCache new]; }); @synchronized(cache) { if([cache objectForKey:jid] != nil) return [cache objectForKey:jid]; } NSMutableDictionary* retval = [NSMutableDictionary new]; NSArray* parts = [self splitString:jid withSeparator:@"/" andMaxSize:2]; retval[@"user"] = [[parts objectAtIndex:0] lowercaseString]; //intended to not break code that expects lowercase if([parts count] > 1 && [[parts objectAtIndex:1] isEqualToString:@""] == NO) retval[@"resource"] = [parts objectAtIndex:1]; //resources are case sensitive //there should never be more than one @ char, but just in case: split only at the first one parts = [self splitString:retval[@"user"] withSeparator:@"@" andMaxSize:2]; if([parts count] > 1) { retval[@"node"] = [[parts objectAtIndex:0] lowercaseString]; //intended to not break code that expects lowercase retval[@"host"] = [[parts objectAtIndex:1] lowercaseString]; //intended to not break code that expects lowercase } else retval[@"host"] = [[parts objectAtIndex:0] lowercaseString]; //intended to not break code that expects lowercase //don't assert to not have a dos vector here, but still log the error if([retval[@"host"] isEqualToString:@""]) DDLogError(@"jid has no host part: %@", jid); //assert on sanity check errors (this checks 'host' and 'user' at once because without node host==user) //MLAssert(![retval[@"host"] isEqualToString:@""], @"jid has no host part!", @{@"jid": jid}); //sanitize retval if([retval[@"node"] isEqualToString:@""]) { [retval removeObjectForKey:@"node"]; retval[@"user"] = retval[@"host"]; //empty node means user==host } if([retval[@"resource"] isEqualToString:@""]) [retval removeObjectForKey:@"resource"]; //cache and return immutable copy @synchronized(cache) { [cache setObject:[retval copy] forKey:jid]; } return [retval copy]; } +(BOOL) isContactBlacklistedForEncryption:(MLContact*) contact { BOOL blacklisted = NO; //cheogram.com does not support OMEMO encryption as it is a PSTN gateway blacklisted = [@"cheogram.com" isEqualToString:[self splitJid:contact.contactJid][@"host"]]; if(blacklisted) DDLogWarn(@"Jid blacklisted for encryption: %@", contact); return blacklisted; } +(void) removeAllShareInteractionsForAccountID:(NSNumber*) accountID { DDLogInfo(@"Removing share interaction for all contacts on account id %@", accountID); for(MLContact* contact in [[DataLayer sharedInstance] contactList]) if(contact.accountID.intValue == accountID.intValue) [contact removeShareInteractions]; } +(void) scheduleBackgroundTask:(BOOL) force { DDLogInfo(@"Scheduling new BackgroundTask with force=%s...", force ? "yes" : "no"); [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSError* error; if(force) { //don't cancel existing task because that could delay our next execution // //cancel existing task (if any) // [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:kBackgroundProcessingTask]; //new task BGProcessingTaskRequest* processingRequest = [[BGProcessingTaskRequest alloc] initWithIdentifier:kBackgroundProcessingTask]; //do the same like the corona warn app from germany which leads to this hint: https://developer.apple.com/forums/thread/134031 processingRequest.earliestBeginDate = nil; processingRequest.requiresNetworkConnectivity = YES; processingRequest.requiresExternalPower = NO; if(![[BGTaskScheduler sharedScheduler] submitTaskRequest:processingRequest error:&error]) { // Errorcodes https://stackoverflow.com/a/58224050/872051 DDLogError(@"Failed to submit BGTask request %@: %@", processingRequest, error); } else DDLogVerbose(@"Success submitting BGTask request %@", processingRequest); } else { //cancel existing task (if any) [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:kBackgroundRefreshingTask]; //new task BGAppRefreshTaskRequest* refreshingRequest = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:kBackgroundRefreshingTask]; //on ios<17 do the same like the corona warn app from germany which leads to this hint: https://developer.apple.com/forums/thread/134031 // if(@available(iOS 17.0, macCatalyst 17.0, *)) // refreshingRequest.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:BGFETCH_DEFAULT_INTERVAL]; // else refreshingRequest.earliestBeginDate = nil; if(![[BGTaskScheduler sharedScheduler] submitTaskRequest:refreshingRequest error:&error]) { // Errorcodes https://stackoverflow.com/a/58224050/872051 DDLogError(@"Failed to submit BGTask request %@: %@", refreshingRequest, error); } else DDLogVerbose(@"Success submitting BGTask request %@", refreshingRequest); } }]; } +(void) clearSyncErrorsOnAppForeground { NSMutableDictionary* syncErrorsDisplayed = [NSMutableDictionary dictionaryWithDictionary:[[HelperTools defaultsDB] objectForKey:@"syncErrorsDisplayed"]]; DDLogInfo(@"Clearing syncError notification states: %@", syncErrorsDisplayed); for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) { syncErrorsDisplayed[account.connectionProperties.identity.jid] = @NO; //also remove pending or delivered sync error notifications //this will delay the delivery of such notifications until 60 seconds after the app moved into the background //rather than being delivered 60 seconds after our first sync attempt failed (wether it was in the appex or mainapp) NSString* syncErrorIdentifier = [NSString stringWithFormat:@"syncError::%@", account.connectionProperties.identity.jid]; [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[syncErrorIdentifier]]; [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[syncErrorIdentifier]]; } [[HelperTools defaultsDB] setObject:syncErrorsDisplayed forKey:@"syncErrorsDisplayed"]; } +(void) removePendingSyncErrorNotifications { NSMutableDictionary* syncErrorsDisplayed = [NSMutableDictionary dictionaryWithDictionary:[[HelperTools defaultsDB] objectForKey:@"syncErrorsDisplayed"]]; DDLogInfo(@"Removing pending syncError notifications, current state: %@", syncErrorsDisplayed); for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) { NSString* syncErrorIdentifier = [NSString stringWithFormat:@"syncError::%@", account.connectionProperties.identity.jid]; [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray* requests) { for(UNNotificationRequest* request in requests) if([request.identifier isEqualToString:syncErrorIdentifier]) { //remove pending but not yet delivered sync error notifications and reset state to "not displayed yet" //this will delay the delivery of such notifications until 60 seconds after our last sync attempt failed //rather than being delivered 60 seconds after our first sync attempt failed //--> better UX syncErrorsDisplayed[account.connectionProperties.identity.jid] = @NO; [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[syncErrorIdentifier]]; } }]; } [[HelperTools defaultsDB] setObject:syncErrorsDisplayed forKey:@"syncErrorsDisplayed"]; } +(void) updateSyncErrorsWithDeleteOnly:(BOOL) removeOnly andWaitForCompletion:(BOOL) waitForCompletion { monal_void_block_t updateSyncErrors = ^{ @synchronized(self) { NSMutableDictionary* syncErrorsDisplayed = [NSMutableDictionary dictionaryWithDictionary:[[HelperTools defaultsDB] objectForKey:@"syncErrorsDisplayed"]]; DDLogInfo(@"Updating syncError notifications: %@", syncErrorsDisplayed); for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) { NSString* syncErrorIdentifier = [NSString stringWithFormat:@"syncError::%@", account.connectionProperties.identity.jid]; //dispatching this to the receive queue isn't neccessary anymore, see comments in account.idle if(account.idle) { //but only do so, if we have connectivity, otherwise just ignore it (the old sync error should still be displayed) if([[MLXMPPManager sharedInstance] hasConnectivity]) { DDLogInfo(@"Removing syncError notification for %@ (now synced)...", account.connectionProperties.identity.jid); [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[syncErrorIdentifier]]; [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[syncErrorIdentifier]]; syncErrorsDisplayed[account.connectionProperties.identity.jid] = @NO; [[HelperTools defaultsDB] setObject:syncErrorsDisplayed forKey:@"syncErrorsDisplayed"]; } } else if(!removeOnly && [self isNotInFocus]) { if([syncErrorsDisplayed[account.connectionProperties.identity.jid] boolValue]) { DDLogWarn(@"NOT posting syncError notification for %@ (already did so since last app foreground)...", account.connectionProperties.identity.jid); continue; } //we always want to post sync errors if we are in the appex (because an incoming push means the server has //*possibly* queued some messages for us) //if we are in the main app we only want to post sync errors if we are in one of these states: //1. we are NOT doing a full reconnect and the smacks queue does not contain some unacked message stanzas having a body //--> (briefly) opening the app while not having an internet connection does not generate sync errors (if no //outgoing message is pending) //2. we are doing a full reconnect --> we always want to post sync erros because we have to rejoin mucs, //set up push etc. and we *really* want to be sure all of these get a chance to complete //NOTE: this conditions are all swapped and ANDed because we want to continue the loop here instead of posting a sync error if(![self isAppExtension] && !account.isDoingFullReconnect && ![account shouldTriggerSyncErrorForImportantUnackedOutgoingStanzas]) { DDLogWarn(@"NOT posting syncError notification for %@ (we are not in the appex, no important stanzas are unacked and we are not doing a full reconnect)...", account.connectionProperties.identity.jid); DDLogDebug(@"[self isAppExtension] == %@, account.isDoingFullReconnect == %@, [account shouldTriggerSyncErrorForImportantUnackedOutgoingStanzas] == %@", bool2str([self isAppExtension]), bool2str(account.isDoingFullReconnect), bool2str([account shouldTriggerSyncErrorForImportantUnackedOutgoingStanzas])); continue; } DDLogWarn(@"Posting syncError notification for %@...", account.connectionProperties.identity.jid); UNMutableNotificationContent* content = [UNMutableNotificationContent new]; content.title = NSLocalizedString(@"Could not synchronize", @""); content.subtitle = account.connectionProperties.identity.jid; content.body = NSLocalizedString(@"Some messages might wait to be retrieved or sent. Please open the app to retry.", @""); content.sound = [UNNotificationSound defaultSound]; content.categoryIdentifier = @"simple"; //we don't know if and when apple will start the background process or when the next push will come in //--> we need a sync error notification to make the user aware of possible issues //BUT: we can delay it for some time and hope a background process/push that removes the notification before it //is displayed at all is started in the meantime (we use 60 seconds here) UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:syncErrorIdentifier content:content trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:60 repeats: NO]]; NSError* error = [self postUserNotificationRequest:request]; if(error) DDLogError(@"Error posting syncError notification: %@", error); else { syncErrorsDisplayed[account.connectionProperties.identity.jid] = @YES; [[HelperTools defaultsDB] setObject:syncErrorsDisplayed forKey:@"syncErrorsDisplayed"]; } } } } }; //dispatch async because we don't want to block the receive/parse/send queue invoking this check if(waitForCompletion) updateSyncErrors(); else dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), updateSyncErrors); } +(BOOL) isInBackground { __block BOOL inBackground = NO; if([HelperTools isAppExtension]) inBackground = YES; else inBackground = [[MLXMPPManager sharedInstance] isBackgrounded]; /* { [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ if([UIApplication sharedApplication].applicationState==UIApplicationStateBackground) inBackground = YES; }]; } */ return inBackground; } +(BOOL) isNotInFocus { __block BOOL isNotInFocus = NO; isNotInFocus |= [HelperTools isAppExtension]; isNotInFocus |= [[MLXMPPManager sharedInstance] isBackgrounded]; isNotInFocus |= [[MLXMPPManager sharedInstance] isNotInFocus]; return isNotInFocus; } +(void) dispatchAsync:(BOOL) async reentrantOnQueue:(dispatch_queue_t _Nullable) queue withBlock:(monal_void_block_t) block { dispatch_queue_t main_queue = dispatch_get_main_queue(); if(!queue) queue = main_queue; //apple docs say that enqueueing blocks for synchronous execution will execute this blocks in the thread the enqueueing came from //(e.g. the tread we are already in). //so when dispatching synchronously from main queue/thread to some "other queue" and from that queue back to the main queue this means: //the block queued for execution in the "other queue" will be executed in the main thread //this holds true even if multiple synchronous queues sit in between the main thread and this dispatchSyncReentrant:onQueue:(main_queue) call //directly call block: //IF: the destination queue is equal to our current queue //OR IF: the destination queue is the main queue and we are already in the main thread (but not the main queue) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" dispatch_queue_t current_queue = dispatch_get_current_queue(); #pragma clang diagnostic pop if(queue == main_queue && [NSThread isMainThread]) block(); else if(current_queue == queue) block(); else { if(async) dispatch_async(queue, block); else dispatch_sync(queue, block); } } +(void) activityLog { BOOL log_activity = NO; #ifdef DEBUG log_activity = YES; #else log_activity = [[HelperTools defaultsDB] boolForKey:@"showLogInSettings"]; #endif if(log_activity) { dispatch_async(dispatch_queue_create_with_target("im.monal.activityLog", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{ unsigned long counter = 1; while(counter++) { DDLogInfo(@"activity: %lu, memory used / available: %.3fMiB / %.3fMiB", counter, [self report_memory], (CGFloat)os_proc_available_memory() / 1048576); [NSThread sleepForTimeInterval:1]; } }); } } +(NSUserDefaults*) defaultsDB { static NSUserDefaults* db; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ db = [[NSUserDefaults alloc] initWithSuiteName:kAppGroup]; }); return db; } +(DDFileLogger*) fileLogger { return _fileLogger; } +(void) setFileLogger:(DDFileLogger*) fileLogger { _fileLogger = fileLogger; } +(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage counter:(uint64_t*) counter andError:(NSError** _Nullable) error { static NSDateFormatter* dateFormatter = nil; static NSString* (^qos2name)(NSUInteger) = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss:SSS"]; [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]]; qos2name = ^(NSUInteger qos) { switch ((qos_class_t) qos) { case QOS_CLASS_USER_INTERACTIVE: return @"QOS_CLASS_USER_INTERACTIVE"; case QOS_CLASS_USER_INITIATED: return @"QOS_CLASS_USER_INITIATED"; case QOS_CLASS_DEFAULT: return @"QOS_CLASS_DEFAULT"; case QOS_CLASS_UTILITY: return @"QOS_CLASS_UTILITY"; case QOS_CLASS_BACKGROUND: return @"QOS_CLASS_BACKGROUND"; default: return [NSString stringWithFormat:@"QOS_UNKNOWN(%lu)", (unsigned long)qos]; } }; }); //construct json dictionary (*counter)++; NSDictionary* representedObject = @{ @"queueThreadLabel": [self getQueueThreadLabelFor:logMessage], @"processType": [self isAppExtension] ? @"appex" : @"mainapp", @"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent], @"counter": [NSNumber numberWithUnsignedLongLong:*counter], @"processID": _processID, @"qosName": qos2name(logMessage.qos), @"representedObject": logMessage.representedObject ? logMessage.representedObject : [NSNull null], }; NSDictionary* msgDict = @{ @"messageFormat": logMessage.messageFormat, @"message": logMessage.message, @"level": [NSNumber numberWithInteger:logMessage.level], @"flag": [NSNumber numberWithInteger:logMessage.flag], @"context": [NSNumber numberWithInteger:logMessage.context], @"file": logMessage.file, @"fileName": logMessage.fileName, @"function": logMessage.function, @"line": [NSNumber numberWithInteger:logMessage.line], @"tag": representedObject, @"options": [NSNumber numberWithInteger:logMessage.options], @"timestamp": [dateFormatter stringFromDate:logMessage.timestamp], @"threadID": logMessage.threadID, @"threadName": logMessage.threadName, @"queueLabel": logMessage.queueLabel, @"qos": [NSNumber numberWithInteger:logMessage.qos], }; //encode json into NSData NSError* writeError = nil; NSData* rawData = [NSJSONSerialization dataWithJSONObject:msgDict options:NSJSONWritingSortedKeys error:&writeError]; if(writeError) { if(error != nil) *error = writeError; return nil; } return rawData; } +(void) flushLogsWithTimeout:(double) timeout { [_stderrRedirector flushWithTimeout:timeout]; [_stdoutRedirector flushWithTimeout:timeout]; [DDLog flushLog]; [MLUDPLogger flushWithTimeout:timeout]; } +(void) configureXcodeLogging { //only start console logger [DDLog addLogger:[DDOSLogger sharedInstance]]; } +(void) configureLogging { //network logger (start as early as possible) MLUDPLogger* udpLogger = [MLUDPLogger new]; [DDLog addLogger:udpLogger]; //redirect stderr containing NSLog() messages _stderrRedirector = [[MLStreamRedirect alloc] initWithStream:stderr]; NSLog(@"stderr redirection complete..."); //redirect stdout for good measure _stdoutRedirector = [[MLStreamRedirect alloc] initWithStream:stdout]; printf("stdout redirection complete..."); NSString* containerUrl = [[HelperTools getContainerURLForPathComponents:@[]] path]; DDLogInfo(@"Logfile dir: %@", containerUrl); //file logger id logFileManager = [[MLLogFileManager alloc] initWithLogsDirectory:containerUrl defaultFileProtectionLevel:NSFileProtectionCompleteUntilFirstUserAuthentication]; logFileManager.maximumNumberOfLogFiles = 4; logFileManager.logFilesDiskQuota = 512 * 1024 * 1024; self.fileLogger = [[DDFileLogger alloc] initWithLogFileManager:logFileManager]; self.fileLogger.doNotReuseLogFiles = NO; self.fileLogger.rollingFrequency = 60 * 60 * 48; // 48 hour rolling self.fileLogger.maximumFileSize = 128 * 1024 * 1024; [DDLog addLogger:self.fileLogger]; DDLogDebug(@"Sorted logfiles: %@", [logFileManager sortedLogFileInfos]); DDLogDebug(@"Current logfile: %@", self.fileLogger.currentLogFileInfo.filePath); NSError* error; NSDictionary* attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:self.fileLogger.currentLogFileInfo.filePath error:&error]; if(error) DDLogError(@"File attributes error: %@", error); else DDLogDebug(@"File attributes: %@", attrs); //log version info as early as possible DDLogInfo(@"Starting: %@", [self appBuildVersionInfoFor:MLVersionTypeLog]); [DDLog flushLog]; DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_USER_INTERACTIVE", QOS_CLASS_USER_INTERACTIVE); DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_USER_INITIATED", QOS_CLASS_USER_INITIATED); DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_DEFAULT", QOS_CLASS_DEFAULT); DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_UTILITY", QOS_CLASS_UTILITY); DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_BACKGROUND", QOS_CLASS_BACKGROUND); //remove old ascii based logfiles for(NSString* file in [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:containerUrl error:nil] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self LIKE %@", @"Monal *.log"]]) { DDLogWarn(@"Removing old ascii logfile: %@/%@", containerUrl, file); [[NSFileManager defaultManager] removeItemAtPath:[containerUrl stringByAppendingPathComponent:file] error:nil]; } //for debugging when upgrading the app NSArray* directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:containerUrl error:nil]; for(NSString* file in directoryContents) DDLogVerbose(@"File %@/%@", containerUrl, file); } +(int) pendingCrashreportCount { KSCrash* handler = [KSCrash sharedInstance]; return handler.reportCount; } +(void) cleanupRawlogCrashcopies { NSError* error; KSCrash* handler = [KSCrash sharedInstance]; NSSet* reportIds = [NSSet setWithArray:[handler reportIDs]]; NSString* reportpath = [[HelperTools getContainerURLForPathComponents:@[@"CrashReports", @"Reports"]] path]; NSArray* directoryContentsReports = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:reportpath error:&error]; if(error != nil) { DDLogError(@"Failed to get directory contents while cleaning up rawlog crashcopies..."); return; } //parts taken from https://github.com/kstenerud/KSCrash/blob/9e72c018a0ba455a89cf5770dea6e1d5258744b6/Source/KSCrash/Recording/KSCrashReportStore.c#L75 char scanFormat[100]; snprintf(scanFormat, sizeof(scanFormat), "%s-log-%%" PRIx64 ".rawlog", _crashBundleName); for(NSString* filename in [directoryContentsReports filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF LIKE %@", [NSString stringWithFormat:@"%s-log-*.rawlog", _crashBundleName]]]) { NSString* file = [NSString stringWithFormat:@"%@/%@", reportpath, filename]; int64_t reportID = 0; sscanf(filename.UTF8String, scanFormat, &reportID); if(reportID == 0) { DDLogError(@"Could not extract crash report id from '%@', ignoring file!", file); continue; } if(![reportIds containsObject:[NSNumber numberWithLongLong:reportID]]) { DDLogInfo(@"Deleting orphan rawlog copy at '%@'...", file); [[NSFileManager defaultManager] removeItemAtPath:file error:&error]; if(error != nil) DDLogError(@"Error cleaning up orphan rawlog copy at '%@', ignoring file!", file); } } } +(void) installCrashHandler { DDLogVerbose(@"KSCrash installing handler with callback: %p", crash_callback); KSCrash* handler = [KSCrash sharedInstance]; handler.basePath = [[HelperTools getContainerURLForPathComponents:@[@"CrashReports"]] path]; handler.monitoring = KSCrashMonitorTypeProductionSafe; //KSCrashMonitorTypeAll handler.onCrash = crash_callback; //this can trigger crashes on macos < 13 (e.g. mac catalyst < 16) (and possibly ios < 16) #if !TARGET_OS_MACCATALYST [handler enableSwapOfCxaThrow]; #endif handler.searchQueueNames = NO; //this is not async safe and can crash :( handler.introspectMemory = YES; handler.addConsoleLogToReport = YES; handler.printPreviousLog = NO; //debug kscrash itself? handler.demangleLanguages = KSCrashDemangleLanguageAll; handler.maxReportCount = 4; handler.deadlockWatchdogInterval = 0; // no main thread watchdog handler.userInfo = @{ @"isAppex": @([self isAppExtension]), @"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent], @"bundleName": nilWrapper([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]), @"appVersion": [self appBuildVersionInfoFor:MLVersionTypeLog], }; //we can not use [KSCrash install] because this uses the bundle names to store our crash reports which are different //in appex and mainapp use the lowlevel C api with dummy bundle name "UnifiedReport" instead handler.monitoring = kscrash_install(_crashBundleName, handler.basePath.UTF8String); if(handler.monitoring == KSCrashMonitorTypeNone) DDLogError(@"Failed to install KSCrash monitors, crash reporting is disabled now!"); else DDLogInfo(@"Crash monitoring active now: %d", handler.monitoring); [HelperTools updateCurrentLogfilePath:self.fileLogger.currentLogFileInfo.filePath]; //store data globally for later retrieval by our crash_callback() (_origProfilePath and _profilePath) NSString* profrawFilePath = [[HelperTools getContainerURLForPathComponents:@[@"default.profraw"]] path]; strncpy(_origProfilePath, profrawFilePath.UTF8String, sizeof(_profilePath)-1); _origProfilePath[sizeof(_origProfilePath)-1] = '\0'; //use the same id for our logfile copy as for the main report (allows to delete all logfile copies for which no crash report exists) //KSCrash increments the id by one every new crash --> the next id used by kscrash will be this one uint64_t nextCrashId = kscrs_getNextCrashReport(NULL) + 1; snprintf(_profilePath, sizeof(_profilePath)-1, "%s/Reports/%s-profile-%016llx.profraw", handler.basePath.UTF8String, _crashBundleName, nextCrashId); _profilePath[sizeof(_profilePath)-1] = '\0'; DDLogVerbose(@"KSCrash: _origProfilePath=%s, _profilePath=%s", _origProfilePath, _profilePath); //clean up orphan rawlog copies [self cleanupRawlogCrashcopies]; NSArray* directoryContentsData = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[[HelperTools getContainerURLForPathComponents:@[@"CrashReports", @"Data"]] path] error:nil]; DDLogDebug(@"KSCrash data files: %@", directoryContentsData); NSArray* directoryContentsReports = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[[HelperTools getContainerURLForPathComponents:@[@"CrashReports", @"Reports"]] path] error:nil]; DDLogDebug(@"KSCrash report files: %@", directoryContentsReports); //[[KSCrash sharedInstance] reportUserException:@"test" reason:@"dummy test" language:@"dylang" lineOfCode:nil stackTrace:nil logAllThreads:NO terminateProgram:YES]; } +(void) updateCurrentLogfilePath:(NSString*) logfilePath { KSCrash* handler = [KSCrash sharedInstance]; //store data globally for later retrieval by our crash_callback() (_origLogfilePath and _logfilePath) strncpy(_origLogfilePath, logfilePath.UTF8String, sizeof(_logfilePath)-1); _origLogfilePath[sizeof(_origLogfilePath)-1] = '\0'; //use the same id for our logfile copy as for the main report (allows to delete all logfile copies for which no crash report exists) //KSCrash increments the id by one every new crash --> the next id used by kscrash will be this one uint64_t nextCrashId = kscrs_getNextCrashReport(NULL) + 1; snprintf(_logfilePath, sizeof(_logfilePath)-1, "%s/Reports/%s-log-%016llx.rawlog", handler.basePath.UTF8String, _crashBundleName, nextCrashId); _logfilePath[sizeof(_logfilePath)-1] = '\0'; DDLogVerbose(@"KSCrash: _origLogfilePath=%s, _logfilePath=%s", _origLogfilePath, _logfilePath); } +(BOOL) isAppExtension { //dispatch once seems to corrupt this check (nearly always return mainapp even if in appex) --> don't use dispatch once static BOOL result = NO; static BOOL calculated = NO; @synchronized(_isAppExtensionLock) { if(calculated) return result; result = [[[NSBundle mainBundle] executablePath] containsString:@".appex/"]; calculated = YES; return result; } } +(NSString*) getEntityCapsHashForIdentities:(NSArray*) identities andFeatures:(NSSet*) features andForms:(NSArray*) forms { // see https://xmpp.org/extensions/xep-0115.html#ver NSMutableString* unhashed = [NSMutableString new]; //generate identities string (must be sorted according to XEP-0115) identities = [identities sortedArrayUsingSelector:@selector(compare:)]; for(NSString* identity in identities) [unhashed appendString:[NSString stringWithFormat:@"%@<", [self _replaceLowerThanInString:identity]]]; //append features string [unhashed appendString:[self generateStringOfFeatureSet:features]]; //append forms string [unhashed appendString:[self generateStringOfCapsForms:forms]]; NSString* hashedBase64 = [self encodeBase64WithData:[self sha1:[unhashed dataUsingEncoding:NSUTF8StringEncoding]]]; DDLogVerbose(@"ver string: unhashed %@, hashed-64 %@", unhashed, hashedBase64); return hashedBase64; } +(NSString*) _replaceLowerThanInString:(NSString*) str { NSMutableString* retval = [str mutableCopy]; [retval replaceOccurrencesOfString:@"<" withString:@"<" options:NSLiteralSearch range:NSMakeRange(0, retval.length)]; return [retval copy]; //make immutable } +(NSSet*) getOwnFeatureSet { static NSSet* featuresSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray* featuresArray = [@[ @"http://jabber.org/protocol/caps", @"http://jabber.org/protocol/disco#info", @"jabber:x:conference", @"jabber:x:oob", @"urn:xmpp:ping", @"urn:xmpp:eme:0", @"urn:xmpp:message-retract:1", @"urn:xmpp:message-correct:0", ] mutableCopy]; if([[HelperTools defaultsDB] boolForKey: @"SendLastUserInteraction"]) [featuresArray addObject:@"urn:xmpp:idle:1"]; if([[HelperTools defaultsDB] boolForKey: @"SendLastChatState"]) [featuresArray addObject:@"http://jabber.org/protocol/chatstates"]; if([[HelperTools defaultsDB] boolForKey: @"SendReceivedMarkers"]) [featuresArray addObject:@"urn:xmpp:receipts"]; if([[HelperTools defaultsDB] boolForKey: @"SendDisplayedMarkers"]) [featuresArray addObject:@"urn:xmpp:chat-markers:0"]; if([[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) [featuresArray addObject:@"jabber:iq:version"]; //voip stuff if([HelperTools shouldProvideVoip]) { [featuresArray addObject:@"urn:xmpp:jingle-message:0"]; [featuresArray addObject:@"urn:xmpp:jingle:1"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:rtp:1"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:rtp:audio"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:rtp:video"]; [featuresArray addObject:@"urn:xmpp:jingle:transports:ice-udp:1"]; [featuresArray addObject:@"urn:ietf:rfc:5888"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:dtls:0"]; [featuresArray addObject:@"urn:ietf:rfc:5576"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]; [featuresArray addObject:@"urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]; } featuresSet = [[NSSet alloc] initWithArray:featuresArray]; }); return featuresSet; } +(NSString*) generateStringOfFeatureSet:(NSSet*) features { // this has to be sorted for the features hash to be correct, see https://xmpp.org/extensions/xep-0115.html#ver NSArray* featuresArray = [[features allObjects] sortedArrayUsingSelector:@selector(compare:)]; NSMutableString* toreturn = [NSMutableString new]; for(NSString* feature in featuresArray) { [toreturn appendString:[self _replaceLowerThanInString:feature]]; [toreturn appendString:@"<"]; } return toreturn; } +(NSString*) generateStringOfCapsForms:(NSArray*) forms { // this has to be sorted for the features hash to be correct, see https://xmpp.org/extensions/xep-0115.html#ver NSMutableString* toreturn = [NSMutableString new]; for(XMPPDataForm* form in [forms sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"formType" ascending:YES selector:@selector(compare:)]]]) { [toreturn appendString:[self _replaceLowerThanInString:form.formType]]; [toreturn appendString:@"<"]; for(NSString* field in [[form allKeys] sortedArrayUsingSelector:@selector(compare:)]) { if([@"FORM_TYPE" isEqualToString:field]) continue; [toreturn appendString:[self _replaceLowerThanInString:field]]; [toreturn appendString:@"<"]; for(NSString* value in [[form getField:field][@"allValues"] sortedArrayUsingSelector:@selector(compare:)]) { [toreturn appendString:[self _replaceLowerThanInString:value]]; [toreturn appendString:@"<"]; } } } return toreturn; } /* * create string containing the info when a user was seen the last time */ +(NSString*) formatLastInteraction:(NSDate*) lastInteraction { // get current timestamp unsigned long currentTimestamp = [HelperTools currentTimestampInSeconds].unsignedLongValue; unsigned long lastInteractionTime = 0; //default is zero which corresponds to "online" // calculate timestamp and clamp it to be not in the future (but only if given) if(lastInteraction && [lastInteraction timeIntervalSince1970] != 0) { //NSDictionary does not support nil, so we're using timeSince1970 + 0 sometimes lastInteractionTime = MIN([HelperTools dateToNSNumberSeconds:lastInteraction].unsignedLongValue, currentTimestamp); } if(lastInteractionTime > 0) { NSString* timeString; long long diff = currentTimestamp - lastInteractionTime; if(diff < 60) { // less than one minute timeString = NSLocalizedString(@"Just seen", @""); } else if(diff < 120) { // less than two minutes timeString = NSLocalizedString(@"Last seen: 1 minute ago", @""); } else if(diff < 3600) { // less than one hour timeString = NSLocalizedString(@"Last seen: %d minutes ago", @""); diff /= 60.0; } else if(diff < 7200) { // less than 2 hours timeString = NSLocalizedString(@"Last seen: 1 hour ago", @""); } else if(diff < 86400) { // less than 24 hours timeString = NSLocalizedString(@"Last seen: %d hours ago", @""); diff /= 3600; } else if(diff < 86400 * 2) { // less than 2 days timeString = NSLocalizedString(@"Last seen: 1 day ago", @""); } else { // more than 2 days timeString = NSLocalizedString(@"Last seen: %d days ago", @""); diff /= 86400; } NSString* lastSeen = [NSString stringWithFormat:timeString, diff]; return [NSString stringWithFormat:@"%@", lastSeen]; } else { return NSLocalizedString(@"Online", @""); } } +(NSString*) stringFromTimeInterval:(NSUInteger) interval { NSUInteger hours = interval / 3600; NSUInteger minutes = (interval % 3600) / 60; NSUInteger seconds = interval % 60; return [NSString stringWithFormat:@"%luh %lumin and %lusec", hours, minutes, seconds]; } +(NSDate*) parseDateTimeString:(NSString*) datetime { static NSDateFormatter* rfc3339DateFormatter; static NSDateFormatter* rfc3339DateFormatter2; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLocale* enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; rfc3339DateFormatter = [NSDateFormatter new]; rfc3339DateFormatter2 = [NSDateFormatter new]; [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSSSSXXXXX"]; [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; [rfc3339DateFormatter2 setLocale:enUSPOSIXLocale]; [rfc3339DateFormatter2 setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; [rfc3339DateFormatter2 setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"]; }); NSDate* retval = [rfc3339DateFormatter dateFromString:datetime]; if(!retval) retval = [rfc3339DateFormatter2 dateFromString:datetime]; return retval; } +(NSString*) generateDateTimeString:(NSDate*) datetime { static NSDateFormatter* rfc3339DateFormatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLocale* enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; rfc3339DateFormatter = [NSDateFormatter new]; [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z"]; [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; }); return [rfc3339DateFormatter stringFromDate:datetime]; } +(NSString*) sanitizeFilePath:(const char* const) file { NSString* fileStr = [NSString stringWithFormat:@"%s", file]; NSArray* filePathComponents = [fileStr pathComponents]; if([filePathComponents count]>1) fileStr = [NSString stringWithFormat:@"%@/%@", filePathComponents[[filePathComponents count]-2], filePathComponents[[filePathComponents count]-1]]; return fileStr; } //don't use this directly, but via createDelayableTimer() makros +(MLDelayableTimer*) startDelayableQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue { if(queue == nil) queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); MLDelayableTimer* timer = [[MLDelayableTimer alloc] initWithHandler:^(MLDelayableTimer* timer){ if(handler) dispatch_async(queue, ^{ DDLogDebug(@"calling handler for timer: %@", timer); handler(); }); } andCancelHandler:^(MLDelayableTimer* timer){ if(cancelHandler) dispatch_async(queue, ^{ DDLogDebug(@"calling cancel block for timer: %@", timer); cancelHandler(); }); } timeout:timeout tolerance:0.1 andDescription:[NSString stringWithFormat:@"created at %@:%d in %s", [self sanitizeFilePath:file], line, func]]; if(timeout < 0.001) { //DDLogVerbose(@"Timer timeout is smaller than 0.001, dispatching handler directly: %@", timer); [timer invalidate]; if(handler) dispatch_async(queue, ^{ handler(); }); return timer; //this timer is not added to a runloop and invalid because the handler already got called } [timer start]; return timer; } //don't use this directly, but via createTimer() makros +(monal_void_block_t) startQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue { MLDelayableTimer* timer = [self startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:file andLine:line andFunc:func onQueue:queue]; return ^{ [timer cancel]; }; } +(AnyPromise*) waitAtLeastSeconds:(NSTimeInterval) seconds forPromise:(AnyPromise*) promise { return PMKWhen(@[promise, PMKAfter(seconds)]).then(^{ return promise; }); } +(NSString*) generateRandomPassword { u_int32_t i=arc4random(); return [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]; } +(NSString*) encodeRandomResource { u_int32_t i=arc4random(); #if TARGET_OS_MACCATALYST NSString* resource = [NSString stringWithFormat:@"Monal-macOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; #else #if IS_QUICKSY NSString* resource = [NSString stringWithFormat:@"Quicksy-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; #else NSString* resource = [NSString stringWithFormat:@"Monal-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; #endif #endif return resource; } +(NSString*) appBuildVersionInfoFor:(MLVersionType) type { @synchronized(_versionInfoCache) { if(_versionInfoCache[@(type)] != nil) return _versionInfoCache[@(type)]; #ifdef IS_ALPHA NSString* rawVersionString = [NSString stringWithFormat:@"Alpha %s (%s %s UTC)", ALPHA_COMMIT_HASH, __DATE__, __TIME__]; #else// IS_ALPHA NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; NSString* rawVersionString = [NSString stringWithFormat:@"%@ %@ (%@)", #ifdef DEBUG @"Beta", #else// DEBUG @"Stable", #endif// DEBUG [infoDict objectForKey:@"CFBundleShortVersionString"], [infoDict objectForKey:@"CFBundleVersion"] ]; #endif// IS_ALPHA if(type == MLVersionTypeIQ) return _versionInfoCache[@(type)] = rawVersionString; else if(type == MLVersionTypeLog) return _versionInfoCache[@(type)] = [NSString stringWithFormat:@"Version %@, %@ on iOS/macOS %@", rawVersionString, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"], [UIDevice currentDevice].systemVersion]; unreachable(@"unknown version type!"); } } +(NSNumber*) currentTimestampInSeconds { return [HelperTools dateToNSNumberSeconds:[NSDate date]]; } +(NSNumber*) dateToNSNumberSeconds:(NSDate*) date { return [NSNumber numberWithUnsignedLong:(unsigned long)date.timeIntervalSince1970]; } +(NSArray* _Nullable) sdp2xml:(NSString*) sdp withInitiator:(BOOL) initiator { DDLogVerbose(@"Parsing SDP string using rust(withInitiator=%@): %@", bool2str(initiator), sdp); __block NSMutableArray* retval = [NSMutableArray new]; MLBasePaser* delegate = [[MLBasePaser alloc] initWithCompletion:^(MLXMLNode* _Nullable parsedElement) { DDLogVerbose(@"Parsed jingle sdp element: %@", parsedElement); [retval addObject:parsedElement]; }]; NSString* xmlString = [JingleSDPBridge getJingleStringForSDPString:sdp withInitiator:initiator]; if(xmlString == nil) return nil; DDLogVerbose(@"Parsing XML string produced by rust sdp parser(withInitiator=%@): %@", bool2str(initiator), xmlString); NSXMLParser* xmlParser = [[NSXMLParser alloc] initWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]]; [xmlParser setShouldProcessNamespaces:YES]; [xmlParser setShouldReportNamespacePrefixes:YES]; //for debugging only [xmlParser setShouldResolveExternalEntities:NO]; [xmlParser setDelegate:delegate]; [xmlParser parse]; //blocking operation return retval; } +(NSString* _Nullable) xml2sdp:(MLXMLNode*) xml withInitiator:(BOOL) initiator { NSString* xmlstr = [[[MLXMLNode alloc] initWithElement:@"root" withAttributes:@{} andChildren:xml.children andData:nil] XMLString]; NSString* retval = [JingleSDPBridge getSDPStringForJingleString:xmlstr withInitiator:initiator]; DDLogVerbose(@"Got sdp string from rust(withInitiator=%@): %@", bool2str(initiator), retval); return retval; } +(MLXMLNode* _Nullable) candidate2xml:(NSString*) candidate withMid:(NSString*) mid pwd:(NSString* _Nullable) pwd ufrag:(NSString* _Nullable) ufrag andInitiator:(BOOL) initiator { //use some dummy sdp string to make our rust sdp parser happy //always use "audio" for our dummy media NSMutableString* sdp = [NSMutableString stringWithFormat:@"v=0\r\n\ o=- 2005859539484728435 2 IN IP4 127.0.0.1\r\n\ s=-\r\n\ t=0 0\r\n\ m=audio 9 UDP/TLS/RTP/SAVPF 0\r\n\ c=IN IP4 0.0.0.0\r\n\ a=mid:%@\r\n\ a=%@\r\n", mid, candidate]; if(pwd != nil) [sdp appendString:[NSString stringWithFormat:@"a=ice-pwd:%@\r\n", pwd]]; if(ufrag != nil) [sdp appendString:[NSString stringWithFormat:@"a=ice-ufrag:%@\r\n", ufrag]]; DDLogVerbose(@"Dummy sdp candidate string for rust parser: %@", sdp); //this result array should only contain one single content node or be nil on parser errors NSArray* xml = [self sdp2xml:sdp withInitiator:initiator]; if(xml == nil) return nil; MLAssert([xml count] == 1, @"Only one single content node expected!", (@{@"xml": xml})); MLXMLNode* contentNode = xml[0]; MLAssert([contentNode check:@"/{urn:xmpp:jingle:1}content"], @"Content node not present!", (@{@"xml": xml})); //remove unwanted description node resulting from our dummy sdp media line above (which is needed for the sdp parser) for(MLXMLNode* node in [contentNode find:@"{urn:xmpp:jingle:apps:rtp:1}description"]) [contentNode removeChildNode:node]; return contentNode; } +(NSString* _Nullable) xml2candidate:(MLXMLNode*) xml withInitiator:(BOOL) initiator { //add dummy description childs to each content element, but don't change the original xml node MLXMLNode* node = [xml copy]; for(MLXMLNode* contentNode in [node find:@"{urn:xmpp:jingle:1}content"]) [contentNode addChildNode:[[MLXMLNode alloc] initWithElement:@"description" andNamespace:@"urn:xmpp:jingle:apps:rtp:1" withAttributes:@{@"media": @"audio"} andChildren:@[] andData:nil]]; NSString* xmlString = [self xml2sdp:node withInitiator:initiator]; //the candidate attribute line should always be the last one (given our current rust parser code), but we try to be more robust here NSArray* lines = [xmlString componentsSeparatedByString:@"\r\n"]; NSString* prefix = @"a=candidate"; for(NSString* line in lines) if(line.length >= prefix.length && [prefix isEqualToString:[line substringWithRange:NSMakeRange(0, prefix.length)]]) return [line substringWithRange:NSMakeRange(2, line.length - 2)]; return nil; } #pragma mark Hashes +(NSData*) sha1:(NSData*) data { if(!data) return nil; NSData* hashed; unsigned char digest[CC_SHA1_DIGEST_LENGTH]; if(CC_SHA1([data bytes], (UInt32)[data length], digest)) hashed = [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; return hashed; } +(NSString*) stringSha1:(NSString*) data { return [self hexadecimalString:[self sha1:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSData*) sha1HmacForKey:(NSData*) key andData:(NSData*) data { if(!key || !data) return nil; unsigned char digest[CC_SHA1_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA1, [key bytes], (UInt32)[key length], [data bytes], (UInt32)[data length], digest); return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; } +(NSString*) stringSha1HmacForKey:(NSString*) key andData:(NSString*) data { if(!key || !data) return nil; return [self hexadecimalString:[self sha1HmacForKey:[key dataUsingEncoding:NSUTF8StringEncoding] andData:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSData*) sha256:(NSData*) data { if(!data) return nil; NSData* hashed; unsigned char digest[CC_SHA256_DIGEST_LENGTH]; if(CC_SHA256([data bytes], (UInt32)[data length], digest)) hashed = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH]; return hashed; } +(NSString*) stringSha256:(NSString*) data { return [self hexadecimalString:[self sha256:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSData*) sha256HmacForKey:(NSData*) key andData:(NSData*) data { if(!key || !data) return nil; unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA256, [key bytes], (UInt32)[key length], [data bytes], (UInt32)[data length], digest); return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH]; } +(NSString*) stringSha256HmacForKey:(NSString*) key andData:(NSString*) data { if(!key || !data) return nil; return [self hexadecimalString:[self sha256HmacForKey:[key dataUsingEncoding:NSUTF8StringEncoding] andData:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSData*) sha512:(NSData*) data { if(!data) return nil; NSData* hashed; unsigned char digest[CC_SHA512_DIGEST_LENGTH]; if(CC_SHA512([data bytes], (UInt32)[data length], digest)) hashed = [NSData dataWithBytes:digest length:CC_SHA512_DIGEST_LENGTH]; return hashed; } +(NSString*) stringSha512:(NSString*) data { return [self hexadecimalString:[self sha512:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSData*) sha512HmacForKey:(NSData*) key andData:(NSData*) data { if(!key || !data) return nil; unsigned char digest[CC_SHA512_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA512, [key bytes], (UInt32)[key length], [data bytes], (UInt32)[data length], digest); return [NSData dataWithBytes:digest length:CC_SHA512_DIGEST_LENGTH]; } +(NSString*) stringSha512HmacForKey:(NSString*) key andData:(NSString*) data { if(!key || !data) return nil; return [self hexadecimalString:[self sha512HmacForKey:[key dataUsingEncoding:NSUTF8StringEncoding] andData:[data dataUsingEncoding:NSUTF8StringEncoding]]]; } +(NSUUID*) dataToUUID:(NSData*) data { NSData* hash = [self sha256:data]; uint8_t* bytes = (uint8_t*)hash.bytes; uint16_t* version = (uint16_t*)(bytes + 6); *version = (*version & 0x0fff) | 0x4000; return [[NSUUID alloc] initWithUUIDBytes:bytes]; } +(NSUUID*) stringToUUID:(NSString*) data { return [self dataToUUID:[data dataUsingEncoding:NSUTF8StringEncoding]]; } #pragma mark base64, hex and other data formats +(NSString*) encodeBase64WithString:(NSString*) strData { NSData* data = [strData dataUsingEncoding:NSUTF8StringEncoding]; return [self encodeBase64WithData:data]; } +(NSString*) encodeBase64WithData:(NSData*) objData { return [objData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; } +(NSData*) dataWithBase64EncodedString:(NSString*) string { return [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters]; } //very fast, taken from https://stackoverflow.com/a/33501154 +(NSString*) hexadecimalString:(NSData*) data { static char _NSData_BytesConversionString_[512] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"; UInt16* mapping = (UInt16*)_NSData_BytesConversionString_; register NSUInteger len = data.length; char* hexChars = (char*)malloc( sizeof(char) * (len*2) ); // --- Coeur's contribution - a safe way to check the allocation if (hexChars == NULL) { // we directly raise an exception instead of using NSAssert to make sure assertion is not disabled as this is irrecoverable [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil]; return nil; } // --- register UInt16* dst = ((UInt16*)hexChars) + len-1; register unsigned char* src = (unsigned char*)data.bytes + len-1; while (len--) *dst-- = mapping[*src--]; NSString* retVal = [[NSString alloc] initWithBytesNoCopy:hexChars length:data.length*2 encoding:NSASCIIStringEncoding freeWhenDone:YES]; return retVal; } +(NSData*) dataWithHexString:(NSString*) hex { char buf[3]; buf[2] = '\0'; if([hex length] % 2 != 00) { DDLogError(@"Hex strings should have an even number of digits"); return [NSData new]; } unsigned char* bytes = malloc([hex length] / 2); if(bytes == NULL) { [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil]; return nil; } unsigned char* bp = bytes; for (unsigned int i = 0; i < [hex length]; i += 2) { buf[0] = (unsigned char) [hex characterAtIndex:i]; buf[1] = (unsigned char) [hex characterAtIndex:i+1]; char* b2 = NULL; *bp++ = (unsigned char) strtol(buf, &b2, 16); if(b2 != buf + 2) { DDLogError(@"String should be all hex digits"); free(bytes); return [NSData new]; } } return [NSData dataWithBytesNoCopy:bytes length:[hex length]/2 freeWhenDone:YES]; } //see https://stackoverflow.com/a/29911397/3528174 +(NSData*) XORData:(NSData*) data1 withData:(NSData*) data2 { const char* data1Bytes = [data1 bytes]; const char* data2Bytes = [data2 bytes]; // Mutable data that individual xor'd bytes will be added to NSMutableData* xorData = [NSMutableData new]; for(NSUInteger i = 0; i < data1.length; i++) { const char xorByte = data1Bytes[i] ^ data2Bytes[i]; [xorData appendBytes:&xorByte length:1]; } return xorData; } #pragma mark omemo stuff +(NSString*) signalHexKeyWithData:(NSData*) data { NSString* hex = [self hexadecimalString:data]; //remove 05 cipher info hex = [hex substringWithRange:NSMakeRange(2, hex.length - 2)]; return hex; } +(NSData*) signalIdentityWithHexKey:(NSString*) hexKey { //add 05 cipher info NSString* hexKeyWithCipherInfo = [NSString stringWithFormat:@"05%@", hexKey]; NSData* identity = [self dataWithHexString:hexKeyWithCipherInfo]; return identity; } +(NSString*) signalHexKeyWithSpacesWithData:(NSData*) data { NSMutableString* hex = [[self signalHexKeyWithData:data] mutableCopy]; unsigned int counter = 0; while(counter <= (hex.length - 2)) { counter+=8; [hex insertString:@" " atIndex:counter]; counter++; } return hex.uppercaseString; } #pragma mark ui stuff +(UIView*) MLCustomViewHeaderWithTitle:(NSString*) title { UIView* tempView = [[UIView alloc]initWithFrame:CGRectMake(0, 200, 300, 244)]; tempView.backgroundColor = [UIColor clearColor]; UILabel* tempLabel = [[UILabel alloc]initWithFrame:CGRectMake(15, 0, 300, 44)]; tempLabel.backgroundColor = [UIColor clearColor]; tempLabel.shadowColor = [UIColor blackColor]; tempLabel.shadowOffset = CGSizeMake(0, 2); tempLabel.textColor = [UIColor whiteColor]; //here you can change the text color of header. tempLabel.font = [UIFont boldSystemFontOfSize:17.0f]; tempLabel.text = title; [tempView addSubview:tempLabel]; tempLabel.textColor = [UIColor darkGrayColor]; tempLabel.text = tempLabel.text.uppercaseString; tempLabel.shadowColor = [UIColor clearColor]; tempLabel.font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; return tempView; } +(CIImage*) createQRCodeFromString:(NSString*) input { NSData* inputAsUTF8 = [input dataUsingEncoding:NSUTF8StringEncoding]; CIFilter* qrCode = [CIFilter QRCodeGenerator]; [qrCode setValue:inputAsUTF8 forKey:@"message"]; [qrCode setValue:@"L" forKey:@"correctionLevel"]; return qrCode.outputImage; } //taken from: https://stackoverflow.com/a/30932216/3528174 +(NSArray*) splitString:(NSString*) string withSeparator:(NSString*) separator andMaxSize:(NSUInteger)size { NSMutableArray* result = [[NSMutableArray alloc]initWithCapacity:size]; NSArray* components = [string componentsSeparatedByString:separator]; if(components.count < size) return components; NSUInteger i = 0; while(i < size-1) { [result addObject:components[i]]; i++; } NSMutableString* lastItem = [[NSMutableString alloc] init]; while(i < components.count) { [lastItem appendString:components[i]]; [lastItem appendString:separator]; i++; } //remove the last separator [result addObject:[lastItem substringToIndex:lastItem.length - 1]]; return result; } //see https://nachtimwald.com/2017/04/02/constant-time-string-comparison-in-c/ +(BOOL) constantTimeCompareAttackerString:(NSString* _Nonnull) str1 withKnownString:(NSString* _Nonnull) str2 { if(str1 == nil || str2 == nil) return NO; const char* s1 = str1.UTF8String; const char* s2 = str2.UTF8String; volatile int m = 0; volatile size_t i = 0; volatile size_t j = 0; volatile size_t k = 0; while(1) { //this will only turn on bits in m, but never turn them off m |= s1[i] ^ s2[j]; // if(s1[i] == '\0') break; i++; //always balance increments even if s2 is shorter than s1 if(s2[j] != '\0') j++; if(s2[j] == '\0') k++; } return m == 0; //check if we never turned on any bit in m } +(BOOL) isIP:(NSString*) host { if([[IPV4 matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) return YES; if([[IPV6_HEX4DECCOMPRESSED matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) return YES; if([[IPV6_6HEX4DEC matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) return YES; if([[IPV6_HEXCOMPRESSED matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) return YES; if([[IPV6 matchesInString:host options:0 range:NSMakeRange(0, [host length])] count] > 0) return YES; return NO; } +(NSURLSession*) createEphemeralURLSession { NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) sessionConfig.requiresDNSSECValidation = YES; return [NSURLSession sessionWithConfiguration:sessionConfig]; } @end