URLHandler.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #include <CommonCrypto/CommonCrypto.h>
  21. #import "URLHandler.h"
  22. #import "ServerAPIConnector.h"
  23. #import "AppDelegate.h"
  24. #import "MyIdentityStore.h"
  25. #import "UIDefines.h"
  26. #import "ContactStore.h"
  27. #import "ShareController.h"
  28. #import "NSString+Hex.h"
  29. #import "ScanIdentityController.h"
  30. #import "LicenseStore.h"
  31. #import "BundleUtil.h"
  32. #import "MDMSetup.h"
  33. #import "WorkDataFetcher.h"
  34. #import "Threema-Swift.h"
  35. #ifdef DEBUG
  36. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  37. #else
  38. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  39. #endif
  40. @implementation URLHandler
  41. + (BOOL)handleURL:(NSURL *)url {
  42. if ([url.scheme hasPrefix:@"threema"]) {
  43. if ([url.host isEqualToString:@"link_mobileno"]) {
  44. NSString *code = [url.query stringByReplacingOccurrencesOfString:@"code=" withString:@""];
  45. DDLogVerbose(@"code: %@", code);
  46. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  47. [conn linkMobileNoWithStore:[MyIdentityStore sharedMyIdentityStore] code:code onCompletion:^(BOOL linked) {
  48. UITabBarController *mainTabBar = [AppDelegate getMainTabBarController];
  49. mainTabBar.selectedIndex = kMyIdentityTabBarIndex;
  50. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"mobileno_linked_title", nil) message:NSLocalizedString(@"mobileno_linked_message", nil) actionOk:nil];
  51. } onError:^(NSError *error) {
  52. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  53. }];
  54. return YES;
  55. } else if ([url.host isEqualToString:@"restore"]) {
  56. /* only react to restore URLs if we're currently presenting the generate key view controller */
  57. AppDelegate *appDelegate = [AppDelegate sharedAppDelegate];
  58. if ([appDelegate isPresentingKeyGeneration]) {
  59. appDelegate.urlRestoreData = [url.query stringByReplacingOccurrencesOfString:@"backup=" withString:@""];
  60. [appDelegate presentIDBackupRestore];
  61. }
  62. return YES;
  63. } else if ([url.host isEqualToString:@"add"] || [url.host isEqualToString:@"compose"]) {
  64. NSDictionary *query = [url.query dictionaryFromQueryComponents];
  65. NSString *targetId = [[query objectForKey:@"id"][0] uppercaseString];
  66. if (targetId.length == kIdentityLen) {
  67. [URLHandler handleAddIdentity:targetId compose:[url.host isEqualToString:@"compose"] query:query];
  68. } else {
  69. /* share with unspecified contact */
  70. if ([url.host isEqualToString:@"compose"]) {
  71. ShareController *shareController = [[ShareController alloc] init];
  72. shareController.text = [query objectForKey:@"text"][0];
  73. if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"])
  74. shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]];
  75. [shareController startShare];
  76. }
  77. }
  78. return YES;
  79. } else if ([url.host isEqualToString:@"license"]) {
  80. NSDictionary *query = [url.query dictionaryFromQueryComponents];
  81. LicenseStore *licenseStore = [LicenseStore sharedLicenseStore];
  82. if ([licenseStore isValid] == NO) {
  83. [licenseStore performLicenseCheckWithCompletion:^(BOOL success) {
  84. if (success) {
  85. if ([LicenseStore requiresLicenseKey] == true) {
  86. NSString *errorDescription = [BundleUtil localizedStringForKey:@"already_licensed"];
  87. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:errorDescription message:@"" actionOk:nil];
  88. }
  89. } else {
  90. NSString *username = [query objectForKey:@"username"][0];
  91. NSString *password = [query objectForKey:@"password"][0];
  92. [licenseStore setLicenseUsername:username];
  93. [licenseStore setLicensePassword:password];
  94. [licenseStore performLicenseCheckWithCompletion:^(BOOL success) {
  95. dispatch_async(dispatch_get_main_queue(), ^{
  96. if (success) {
  97. [WorkDataFetcher checkUpdateThreemaMDM:^{
  98. dispatch_async(dispatch_get_main_queue(), ^{
  99. if ([[AppDelegate sharedAppDelegate] isPresentingEnterLicense]) {
  100. UIViewController *currentVC = [AppDelegate sharedAppDelegate].window.rootViewController;
  101. [currentVC dismissViewControllerAnimated:YES completion:^{
  102. NSLog(@"Url handler");
  103. [AppDelegate setupConnection];
  104. }];
  105. }
  106. });
  107. } onError:^(NSError *error) {
  108. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil];
  109. }];
  110. } else {
  111. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil];
  112. }
  113. });
  114. }];
  115. }
  116. }];
  117. } else {
  118. if ([LicenseStore requiresLicenseKey] == true) {
  119. NSString *errorDescription = [BundleUtil localizedStringForKey:@"already_licensed"];
  120. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:errorDescription message:@"" actionOk:nil];
  121. }
  122. }
  123. return YES;
  124. }
  125. } else if ([url.scheme isEqualToString:@"file"]) {
  126. ShareController *shareController = [[ShareController alloc] init];
  127. shareController.url = url;
  128. [shareController startShare];
  129. return YES;
  130. } else if (([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) && [[url.host lowercaseString] isEqualToString:@"threema.id"]) {
  131. [self handleThreemaDotIdUrl:url hideAppChooser:false];
  132. return YES;
  133. }
  134. return NO;
  135. }
  136. + (void)handleThreemaDotIdUrl:(NSURL*)url hideAppChooser:(BOOL)hideAppChooser {
  137. BOOL composeWithChooser = false;
  138. NSString *targetId = [[url.path substringFromIndex:1] uppercaseString];
  139. if (targetId.length != kIdentityLen) {
  140. if (![[[url.path substringFromIndex:1] lowercaseString] isEqualToString:@"compose"]) {
  141. return;
  142. }
  143. composeWithChooser = true;
  144. }
  145. // Check if the "other" app (Work if we are not the Work app, or vice versa) is also installed.
  146. // If so, we need to prompt the user for what to do.
  147. BOOL mustDisplayAppChooser = NO;
  148. BOOL isWorkApp = [LicenseStore requiresLicenseKey];
  149. if (isWorkApp) {
  150. // This is the Work app. Check if the regular app is installed.
  151. if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"threema://app"]]) {
  152. mustDisplayAppChooser = YES;
  153. }
  154. } else {
  155. // This is the regular app. Check if the Work app is installed.
  156. if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"threemawork://app"]]) {
  157. mustDisplayAppChooser = YES;
  158. }
  159. }
  160. NSString *text = [[url.query dictionaryFromQueryComponents] objectForKey:@"text"][0];
  161. text = [text stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
  162. if (mustDisplayAppChooser && hideAppChooser == false) {
  163. NSString *newQuery;
  164. if (composeWithChooser) {
  165. if (text) {
  166. newQuery = [NSString stringWithFormat:@"text=%@", text];
  167. } else {
  168. // there is no text, we can't show the chooser
  169. return;
  170. }
  171. } else {
  172. if (text) {
  173. newQuery = [NSString stringWithFormat:@"id=%@&text=%@", targetId, text];
  174. } else {
  175. newQuery = [NSString stringWithFormat:@"id=%@", targetId];
  176. }
  177. }
  178. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Open in ...", nil) message:url.absoluteString preferredStyle:UIAlertControllerStyleAlert];
  179. [alertController addAction:[UIAlertAction actionWithTitle:@"Threema" style:0 handler:^(UIAlertAction * _Nonnull action) {
  180. if (!isWorkApp) {
  181. if (composeWithChooser) {
  182. [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]];
  183. } else {
  184. [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]];
  185. }
  186. } else {
  187. NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"threema://compose?%@", newQuery]];
  188. [[UIApplication sharedApplication] openURL:newUrl options:@{} completionHandler:nil];
  189. }
  190. }]];
  191. [alertController addAction:[UIAlertAction actionWithTitle:@"Threema Work" style:0 handler:^(UIAlertAction * _Nonnull action) {
  192. if (isWorkApp) {
  193. if (composeWithChooser) {
  194. [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]];
  195. } else {
  196. [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]];
  197. }
  198. } else {
  199. NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"threemawork://compose?%@", newQuery]];
  200. [[UIApplication sharedApplication] openURL:newUrl options:@{} completionHandler:nil];
  201. }
  202. }]];
  203. [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {}]];
  204. [[[AppDelegate sharedAppDelegate] currentTopViewController] presentViewController:alertController animated:YES completion:nil];
  205. } else {
  206. if (composeWithChooser) {
  207. [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]];
  208. } else {
  209. [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]];
  210. }
  211. }
  212. }
  213. + (void)handleAddIdentity:(NSString*)targetId compose:(BOOL)compose query:(NSDictionary*)query {
  214. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:NO];
  215. if ([mdmSetup disableAddContact]) {
  216. /* Ensure this contact already exists, as we are not allowed to add any new ones */
  217. if ([[ContactStore sharedContactStore] contactForIdentity:targetId] == nil) {
  218. return;
  219. }
  220. }
  221. if ([targetId isEqualToString:[[MyIdentityStore sharedMyIdentityStore] identity]]) {
  222. return;
  223. }
  224. /* add this ID to the contacts */
  225. [[ContactStore sharedContactStore] addContactWithIdentity:targetId verificationLevel:kVerificationLevelUnverified onCompletion:^(Contact *contact, BOOL alreadyExists) {
  226. if (compose && [query objectForKey:@"text"][0] != nil) {
  227. ShareController *shareController = [[ShareController alloc] init];
  228. shareController.contact = contact;
  229. shareController.text = [query objectForKey:@"text"][0];
  230. if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"]) {
  231. shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]];
  232. }
  233. [shareController startShare];
  234. } else {
  235. /* just show contact details */
  236. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationShowContact object:nil userInfo:[NSDictionary dictionaryWithObject:contact forKey:kKeyContact]];
  237. }
  238. } onError:^(NSError *error) {
  239. if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == 404) {
  240. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"identity_not_found_title", nil) message:NSLocalizedString(@"identity_not_found_message", nil) actionOk:nil];
  241. } else {
  242. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  243. }
  244. }];
  245. }
  246. + (void)handleComposeWithChooser:(NSString *)text query:(NSDictionary*)query {
  247. ShareController *shareController = [[ShareController alloc] init];
  248. shareController.text = [query objectForKey:@"text"][0];
  249. if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"]) {
  250. shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]];
  251. }
  252. [shareController startShare];
  253. }
  254. + (BOOL)handleShortCutItem:(UIApplicationShortcutItem *)shortCutItem {
  255. if ([shortCutItem.type isEqualToString:@"ch.threema.newmessage"]) {
  256. [self composeMessage];
  257. return YES;
  258. } else if ([shortCutItem.type isEqualToString:@"ch.threema.myid"]) {
  259. UITabBarController *mainTabBar = [AppDelegate getMainTabBarController];
  260. mainTabBar.selectedIndex = kMyIdentityTabBarIndex;
  261. return YES;
  262. } else if ([shortCutItem.type isEqualToString:@"ch.threema.scanid"]) {
  263. if ([ScanIdentityController canScan] == NO) {
  264. return NO;
  265. }
  266. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false];
  267. if (mdmSetup.disableAddContact == true) {
  268. return NO;
  269. }
  270. UITabBarController *mainTabBar = [AppDelegate getMainTabBarController];
  271. ScanIdentityController *scanController = [[ScanIdentityController alloc] init];
  272. scanController.containingViewController = mainTabBar;
  273. scanController.expectedIdentity = nil;
  274. scanController.popupScanResults = YES;
  275. [scanController startScan];
  276. return YES;
  277. }
  278. return NO;
  279. }
  280. + (void)composeMessage {
  281. ShareController *shareController = [[ShareController alloc] init];
  282. [shareController startShare];
  283. }
  284. + (UIImage*)decryptPasteboardImageWithKey:(NSString*)keyHex {
  285. if (keyHex.length == 0) {
  286. /* no key - assume unencrypted image on pasteboard */
  287. return [[UIPasteboard generalPasteboard] image];
  288. }
  289. NSData *key = [keyHex decodeHex];
  290. if (key.length != kCCKeySizeAES256)
  291. return nil;
  292. NSData *imageDataEncrypted = [[UIPasteboard generalPasteboard] dataForPasteboardType:PASTEBOARD_IMAGE_UTI];
  293. if (imageDataEncrypted == nil)
  294. return nil;
  295. char *imagebuf = malloc(imageDataEncrypted.length);
  296. if (imagebuf == NULL)
  297. return nil;
  298. size_t size_out = 0;
  299. if (CCCrypt(kCCDecrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding, key.bytes, key.length, NULL,
  300. imageDataEncrypted.bytes, imageDataEncrypted.length, imagebuf, imageDataEncrypted.length, &size_out) != kCCSuccess) {
  301. DDLogWarn(@"Pasteboard image decryption failed");
  302. free(imagebuf);
  303. return nil;
  304. }
  305. DDLogInfo(@"Pasteboard image decrypted successfully (%zu bytes)", size_out);
  306. NSData *imageData = [NSData dataWithBytesNoCopy:imagebuf length:size_out freeWhenDone:YES];
  307. return [UIImage imageWithData:imageData];
  308. }
  309. @end