3065 lines
130 KiB
Objective-C
3065 lines
130 KiB
Objective-C
//
|
|
// HelperTools.m
|
|
// Monal
|
|
//
|
|
// Created by Friedrich Altheide on 08.07.20.
|
|
// Copyright © 2020 Monal.im. All rights reserved.
|
|
//
|
|
|
|
#include <stdio.h>
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
#include <sys/sysctl.h>
|
|
#include <sys/stat.h>
|
|
#include <mach/mach.h>
|
|
#include <mach/mach_error.h>
|
|
#include <mach/mach_traps.h>
|
|
#include <os/proc.h>
|
|
#include <objc/runtime.h>
|
|
#include <objc/message.h>
|
|
#include <objc/objc-exception.h>
|
|
#import <sys/qos.h>
|
|
#import <BackgroundTasks/BackgroundTasks.h>
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <CommonCrypto/CommonHMAC.h>
|
|
#import <MapKit/MapKit.h>
|
|
#import <MobileCoreServices/MobileCoreServices.h>
|
|
#import <KSCrash/KSCrash.h>
|
|
#import <KSCrash/KSCrashC.h>
|
|
//can not be imported, use extern declaration instead
|
|
//#import <KSCrash/Recording/KSCrashReportStore.h>
|
|
extern int64_t kscrs_getNextCrashReport(char* crashReportPathBuffer);
|
|
#import <monalxmpp/monalxmpp-Swift.h>
|
|
#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<NSString*, NSString*>*) 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<NSString*, NSString*>*) 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<NSString*>*) 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:@"<plist" intoString:nil];
|
|
NSString* plistStr;
|
|
[plistScanner scanUpToString:@"</plist>" intoString:&plistStr];
|
|
plistStr = [NSString stringWithFormat:@"%@</plist>", 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 <NSSecureCoding> 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<NSString*, NSString*>*) 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<NSString*, NSString*>* 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<DDLogFileManager> 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<MLXMLNode*>* _Nullable) sdp2xml:(NSString*) sdp withInitiator:(BOOL) initiator
|
|
{
|
|
DDLogVerbose(@"Parsing SDP string using rust(withInitiator=%@): %@", bool2str(initiator), sdp);
|
|
__block NSMutableArray<MLXMLNode*>* 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<CIQRCodeGenerator>* 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
|