clipboard_recent_content_impl_ios.mm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. // Copyright 2017 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h"
  5. #import <MobileCoreServices/MobileCoreServices.h>
  6. #import <UIKit/UIKit.h>
  7. #import "base/mac/foundation_util.h"
  8. #include "base/notreached.h"
  9. #include "base/strings/sys_string_conversions.h"
  10. #include "base/system/sys_info.h"
  11. #if !defined(__has_feature) || !__has_feature(objc_arc)
  12. #error "This file requires ARC support."
  13. #endif
  14. ContentType const ContentTypeURL = @"ContentTypeURL";
  15. ContentType const ContentTypeText = @"ContentTypeString";
  16. ContentType const ContentTypeImage = @"ContentTypeImage";
  17. namespace {
  18. // Key used to store the pasteboard's current change count. If when resuming
  19. // chrome the pasteboard's change count is different from the stored one, then
  20. // it means that the pasteboard's content has changed.
  21. NSString* const kPasteboardChangeCountKey = @"PasteboardChangeCount";
  22. // Key used to store the last date at which it was detected that the pasteboard
  23. // changed. It is used to evaluate the age of the pasteboard's content.
  24. NSString* const kPasteboardChangeDateKey = @"PasteboardChangeDate";
  25. // Default Scheme to use for urls with no scheme.
  26. NSString* const kDefaultScheme = @"https";
  27. } // namespace
  28. @interface ClipboardRecentContentImplIOS ()
  29. // The user defaults from the app group used to optimize the pasteboard change
  30. // detection.
  31. @property(nonatomic, strong) NSUserDefaults* sharedUserDefaults;
  32. // The pasteboard's change count. Increases everytime the pasteboard changes.
  33. @property(nonatomic) NSInteger lastPasteboardChangeCount;
  34. // Contains the authorized schemes for URLs.
  35. @property(nonatomic, readonly) NSSet* authorizedSchemes;
  36. // Delegate for metrics.
  37. @property(nonatomic, strong) id<ClipboardRecentContentDelegate> delegate;
  38. // Maximum age of clipboard in seconds.
  39. @property(nonatomic, readonly) NSTimeInterval maximumAgeOfClipboard;
  40. // A cached version of an already-retrieved URL. This prevents subsequent URL
  41. // requests from triggering the iOS 14 pasteboard notification.
  42. @property(nonatomic, strong) NSURL* cachedURL;
  43. // A cached version of an already-retrieved string. This prevents subsequent
  44. // string requests from triggering the iOS 14 pasteboard notification.
  45. @property(nonatomic, copy) NSString* cachedText;
  46. // A cached version of an already-retrieved image. This prevents subsequent
  47. // image requests from triggering the iOS 14 pasteboard notification.
  48. @property(nonatomic, strong) UIImage* cachedImage;
  49. // A cached set of the content types currently being used for the clipboard.
  50. @property(nonatomic, strong) NSSet<ContentType>* cachedContentTypes;
  51. // If the content of the pasteboard has changed, updates the change count
  52. // and change date.
  53. - (void)updateIfNeeded;
  54. // Returns whether the pasteboard changed since the last time a pasteboard
  55. // change was detected.
  56. - (BOOL)hasPasteboardChanged;
  57. // Loads information from the user defaults about the latest pasteboard entry.
  58. - (void)loadFromUserDefaults;
  59. // Returns the URL contained in the clipboard (if any).
  60. - (NSURL*)URLFromPasteboard;
  61. // Returns the uptime.
  62. - (NSTimeInterval)uptime;
  63. // Returns whether the value of the clipboard should be returned.
  64. - (BOOL)shouldReturnValueOfClipboard;
  65. // Calls |completionHandler| with the result of whether or not the clipboard
  66. // currently contains data matching |contentType|.
  67. - (void)checkForContentType:(ContentType)contentType
  68. completionHandler:(void (^)(BOOL))completionHandler;
  69. // Checks the clipboard for content matching |types| and calls
  70. // |completionHandler| once all types are checked. This method is called
  71. // recursively and partial results are passed in |results| until all types have
  72. // been checked.
  73. - (void)
  74. hasContentMatchingRemainingTypes:(NSSet<ContentType>*)types
  75. results:
  76. (NSMutableDictionary<ContentType, NSNumber*>*)
  77. results
  78. completionHandler:
  79. (void (^)(NSSet<ContentType>*))completionHandler;
  80. @end
  81. @implementation ClipboardRecentContentImplIOS
  82. - (instancetype)initWithMaxAge:(NSTimeInterval)maxAge
  83. authorizedSchemes:(NSSet<NSString*>*)authorizedSchemes
  84. userDefaults:(NSUserDefaults*)groupUserDefaults
  85. delegate:(id<ClipboardRecentContentDelegate>)delegate {
  86. self = [super init];
  87. if (self) {
  88. _maximumAgeOfClipboard = maxAge;
  89. _delegate = delegate;
  90. _authorizedSchemes = authorizedSchemes;
  91. _sharedUserDefaults = groupUserDefaults;
  92. _lastPasteboardChangeCount = NSIntegerMax;
  93. [self loadFromUserDefaults];
  94. [self updateIfNeeded];
  95. [self updateCachedClipboardState];
  96. // Makes sure |last_pasteboard_change_count_| was properly initialized.
  97. DCHECK_NE(_lastPasteboardChangeCount, NSIntegerMax);
  98. [[NSNotificationCenter defaultCenter]
  99. addObserver:self
  100. selector:@selector(didBecomeActive:)
  101. name:UIApplicationDidBecomeActiveNotification
  102. object:nil];
  103. [[NSNotificationCenter defaultCenter]
  104. addObserver:self
  105. selector:@selector(pasteboardDidChange:)
  106. name:UIPasteboardChangedNotification
  107. object:nil];
  108. }
  109. return self;
  110. }
  111. - (void)dealloc {
  112. [[NSNotificationCenter defaultCenter] removeObserver:self];
  113. }
  114. - (void)didBecomeActive:(NSNotification*)notification {
  115. [self loadFromUserDefaults];
  116. [self updateIfNeeded];
  117. [self updateCachedClipboardState];
  118. }
  119. - (BOOL)hasPasteboardChanged {
  120. return UIPasteboard.generalPasteboard.changeCount !=
  121. self.lastPasteboardChangeCount;
  122. }
  123. - (void)pasteboardDidChange:(NSNotification*)notification {
  124. [self updateCachedClipboardState];
  125. }
  126. - (NSURL*)recentURLFromClipboard {
  127. [self updateIfNeeded];
  128. if (![self shouldReturnValueOfClipboard])
  129. return nil;
  130. if (@available(iOS 14, *)) {
  131. // On iOS 14, don't actually access the pasteboard in this method. This
  132. // prevents the pasteboard access notification from appearing.
  133. } else {
  134. if (!self.cachedURL) {
  135. self.cachedURL = [self URLFromPasteboard];
  136. }
  137. }
  138. return self.cachedURL;
  139. }
  140. - (NSString*)recentTextFromClipboard {
  141. [self updateIfNeeded];
  142. if (![self shouldReturnValueOfClipboard])
  143. return nil;
  144. if (@available(iOS 14, *)) {
  145. // On iOS 14, don't actually access the pasteboard in this method. This
  146. // prevents the pasteboard access notification from appearing.
  147. } else {
  148. if (!self.cachedText) {
  149. self.cachedText = UIPasteboard.generalPasteboard.string;
  150. }
  151. }
  152. return self.cachedText;
  153. }
  154. - (UIImage*)recentImageFromClipboard {
  155. [self updateIfNeeded];
  156. if (![self shouldReturnValueOfClipboard])
  157. return nil;
  158. if (@available(iOS 14, *)) {
  159. // On iOS 14, don't actually access the pasteboard in this method. This
  160. // prevents the pasteboard access notification from appearing.
  161. } else {
  162. if (!self.cachedImage) {
  163. self.cachedImage = UIPasteboard.generalPasteboard.image;
  164. }
  165. }
  166. return self.cachedImage;
  167. }
  168. - (void)updateCachedClipboardState {
  169. self.cachedContentTypes = nil;
  170. NSSet<ContentType>* desiredContentTypes = [NSSet
  171. setWithArray:@[ ContentTypeImage, ContentTypeURL, ContentTypeText ]];
  172. __weak __typeof(self) weakSelf = self;
  173. [self hasContentMatchingTypes:desiredContentTypes
  174. completionHandler:^(NSSet<ContentType>* results) {
  175. weakSelf.cachedContentTypes = results;
  176. }];
  177. }
  178. - (NSSet<ContentType>*)cachedClipboardContentTypes {
  179. if (![self shouldReturnValueOfClipboard])
  180. return nil;
  181. return self.cachedContentTypes;
  182. }
  183. - (void)hasContentMatchingTypes:(NSSet<ContentType>*)types
  184. completionHandler:
  185. (void (^)(NSSet<ContentType>*))completionHandler {
  186. [self updateIfNeeded];
  187. if (![self shouldReturnValueOfClipboard] || ![types count]) {
  188. completionHandler([NSSet set]);
  189. return;
  190. }
  191. [self hasContentMatchingRemainingTypes:types
  192. results:[[NSMutableDictionary alloc] init]
  193. completionHandler:completionHandler];
  194. }
  195. - (void)
  196. hasContentMatchingRemainingTypes:(NSSet<ContentType>*)types
  197. results:
  198. (NSMutableDictionary<ContentType, NSNumber*>*)
  199. results
  200. completionHandler:
  201. (void (^)(NSSet<ContentType>*))completionHandler {
  202. if ([types count] == 0) {
  203. NSMutableSet<ContentType>* matchingTypes = [NSMutableSet set];
  204. for (ContentType type in results) {
  205. if ([results[type] boolValue]) {
  206. [matchingTypes addObject:type];
  207. }
  208. }
  209. completionHandler(matchingTypes);
  210. return;
  211. }
  212. __weak __typeof(self) weakSelf = self;
  213. ContentType type = [types anyObject];
  214. [self checkForContentType:type
  215. completionHandler:^(BOOL hasType) {
  216. results[type] = @(hasType);
  217. NSMutableSet* remainingTypes = [types mutableCopy];
  218. [remainingTypes removeObject:type];
  219. [weakSelf hasContentMatchingRemainingTypes:remainingTypes
  220. results:results
  221. completionHandler:completionHandler];
  222. }];
  223. }
  224. - (void)checkForContentType:(ContentType)contentType
  225. completionHandler:(void (^)(BOOL))completionHandler {
  226. if ([contentType isEqualToString:ContentTypeText]) {
  227. [self hasRecentTextFromClipboardInternal:^(BOOL hasText) {
  228. completionHandler(hasText);
  229. }];
  230. } else if ([contentType isEqualToString:ContentTypeURL]) {
  231. [self hasRecentURLFromClipboardInternal:^(BOOL hasURL) {
  232. completionHandler(hasURL);
  233. }];
  234. } else if ([contentType isEqualToString:ContentTypeImage]) {
  235. [self hasRecentImageFromClipboardInternal:^(BOOL hasImage) {
  236. completionHandler(hasImage);
  237. }];
  238. } else {
  239. NOTREACHED() << contentType;
  240. }
  241. }
  242. - (void)hasRecentURLFromClipboardInternal:(void (^)(BOOL))callback {
  243. DCHECK(callback);
  244. if (@available(iOS 14, *)) {
  245. // Use cached value if it exists
  246. if (self.cachedURL) {
  247. callback(YES);
  248. return;
  249. }
  250. #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
  251. NSSet<UIPasteboardDetectionPattern>* urlPattern =
  252. [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebURL];
  253. [UIPasteboard.generalPasteboard
  254. detectPatternsForPatterns:urlPattern
  255. completionHandler:^(
  256. NSSet<UIPasteboardDetectionPattern>* patterns,
  257. NSError* error) {
  258. callback([patterns
  259. containsObject:
  260. UIPasteboardDetectionPatternProbableWebURL]);
  261. }];
  262. #else
  263. // To prevent clipboard notification from appearing on iOS 14 with iOS 13
  264. // SDK, use the -hasURLs property to check for URL existence. This will
  265. // cause crbug.com/1033935 to reappear in code using this method (also see
  266. // the comments in -URLFromPasteboard in this file), but that is preferable
  267. // to the notificatio appearing when it shouldn't.
  268. callback(UIPasteboard.generalPasteboard.hasURLs);
  269. #endif
  270. } else {
  271. callback([self recentURLFromClipboard] != nil);
  272. }
  273. }
  274. - (void)hasRecentTextFromClipboardInternal:(void (^)(BOOL))callback {
  275. DCHECK(callback);
  276. if (@available(iOS 14, *)) {
  277. // Use cached value if it exists
  278. if (self.cachedText) {
  279. callback(YES);
  280. return;
  281. }
  282. #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
  283. NSSet<UIPasteboardDetectionPattern>* textPattern =
  284. [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebSearch];
  285. [UIPasteboard.generalPasteboard
  286. detectPatternsForPatterns:textPattern
  287. completionHandler:^(
  288. NSSet<UIPasteboardDetectionPattern>* patterns,
  289. NSError* error) {
  290. callback([patterns
  291. containsObject:
  292. UIPasteboardDetectionPatternProbableWebSearch]);
  293. }];
  294. #else
  295. callback(UIPasteboard.generalPasteboard.hasStrings);
  296. #endif
  297. } else {
  298. callback([self recentTextFromClipboard] != nil);
  299. }
  300. }
  301. - (void)hasRecentImageFromClipboardInternal:(void (^)(BOOL))callback {
  302. DCHECK(callback);
  303. if (@available(iOS 14, *)) {
  304. // Use cached value if it exists
  305. if (self.cachedImage) {
  306. callback(YES);
  307. return;
  308. }
  309. callback(UIPasteboard.generalPasteboard.hasImages);
  310. } else {
  311. callback([self recentImageFromClipboard] != nil);
  312. }
  313. }
  314. - (void)recentURLFromClipboardAsync:(void (^)(NSURL*))callback {
  315. DCHECK(callback);
  316. if (@available(iOS 14, *)) {
  317. [self updateIfNeeded];
  318. if (![self shouldReturnValueOfClipboard]) {
  319. callback(nil);
  320. return;
  321. }
  322. // Use cached value if it exists.
  323. if (self.cachedURL) {
  324. callback(self.cachedURL);
  325. return;
  326. }
  327. #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
  328. __weak __typeof(self) weakSelf = self;
  329. NSSet<UIPasteboardDetectionPattern>* urlPattern =
  330. [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebURL];
  331. [UIPasteboard.generalPasteboard
  332. detectValuesForPatterns:urlPattern
  333. completionHandler:^(
  334. NSDictionary<UIPasteboardDetectionPattern, id>* values,
  335. NSError* error) {
  336. // On iOS 16, users can deny access to the clipboard.
  337. if (error) {
  338. weakSelf.cachedURL = nil;
  339. callback(nil);
  340. return;
  341. }
  342. NSURL* url = [NSURL
  343. URLWithString:
  344. values[UIPasteboardDetectionPatternProbableWebURL]];
  345. // |detectValuesForPatterns:| will return a url even if the url
  346. // is missing a scheme. In this case, default to https.
  347. if (url && url.scheme == nil) {
  348. NSURLComponents* components =
  349. [[NSURLComponents alloc] initWithURL:url
  350. resolvingAgainstBaseURL:NO];
  351. components.scheme = kDefaultScheme;
  352. url = components.URL;
  353. }
  354. if (![self.authorizedSchemes containsObject:url.scheme]) {
  355. weakSelf.cachedURL = nil;
  356. callback(nil);
  357. } else {
  358. weakSelf.cachedURL = url;
  359. callback(url);
  360. }
  361. }];
  362. #else
  363. callback([self recentURLFromClipboard]);
  364. #endif
  365. } else {
  366. callback([self recentURLFromClipboard]);
  367. }
  368. }
  369. - (void)recentTextFromClipboardAsync:(void (^)(NSString*))callback {
  370. DCHECK(callback);
  371. if (@available(iOS 14, *)) {
  372. [self updateIfNeeded];
  373. if (![self shouldReturnValueOfClipboard]) {
  374. callback(nil);
  375. return;
  376. }
  377. // Use cached value if it exists.
  378. if (self.cachedText) {
  379. callback(self.cachedText);
  380. return;
  381. }
  382. #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
  383. __weak __typeof(self) weakSelf = self;
  384. NSSet<UIPasteboardDetectionPattern>* textPattern =
  385. [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebSearch];
  386. [UIPasteboard.generalPasteboard
  387. detectValuesForPatterns:textPattern
  388. completionHandler:^(
  389. NSDictionary<UIPasteboardDetectionPattern, id>* values,
  390. NSError* error) {
  391. NSString* text =
  392. values[UIPasteboardDetectionPatternProbableWebSearch];
  393. weakSelf.cachedText = text;
  394. callback(text);
  395. }];
  396. #else
  397. callback([self recentTextFromClipboard]);
  398. #endif
  399. } else {
  400. callback([self recentTextFromClipboard]);
  401. }
  402. }
  403. - (void)recentImageFromClipboardAsync:(void (^)(UIImage*))callback {
  404. DCHECK(callback);
  405. [self updateIfNeeded];
  406. if (![self shouldReturnValueOfClipboard]) {
  407. callback(nil);
  408. return;
  409. }
  410. if (!self.cachedImage) {
  411. self.cachedImage = UIPasteboard.generalPasteboard.image;
  412. }
  413. callback(self.cachedImage);
  414. }
  415. - (NSTimeInterval)clipboardContentAge {
  416. return -[self.lastPasteboardChangeDate timeIntervalSinceNow];
  417. }
  418. - (BOOL)shouldReturnValueOfClipboard {
  419. if ([self clipboardContentAge] > self.maximumAgeOfClipboard)
  420. return NO;
  421. // It is the common convention on iOS that password managers tag confidential
  422. // data with the flavor "org.nspasteboard.ConcealedType". Obey this
  423. // convention; the user doesn't want for their confidential data to be
  424. // suggested as a search, anyway. See http://nspasteboard.org/ for more info.
  425. NSArray<NSString*>* types =
  426. [[UIPasteboard generalPasteboard] pasteboardTypes];
  427. if ([types containsObject:@"org.nspasteboard.ConcealedType"])
  428. return NO;
  429. return YES;
  430. }
  431. - (void)suppressClipboardContent {
  432. // User cleared the user data. The pasteboard entry must be removed from the
  433. // omnibox list. Force entry expiration by setting copy date to 1970.
  434. self.lastPasteboardChangeDate =
  435. [[NSDate alloc] initWithTimeIntervalSince1970:0];
  436. [self saveToUserDefaults];
  437. }
  438. - (void)updateIfNeeded {
  439. if (![self hasPasteboardChanged]) {
  440. return;
  441. }
  442. self.lastPasteboardChangeDate = [NSDate date];
  443. self.lastPasteboardChangeCount = [UIPasteboard generalPasteboard].changeCount;
  444. // Clear the cache because the pasteboard data has changed.
  445. self.cachedURL = nil;
  446. self.cachedText = nil;
  447. self.cachedImage = nil;
  448. [self.delegate onClipboardChanged];
  449. [self saveToUserDefaults];
  450. }
  451. - (NSURL*)URLFromPasteboard {
  452. NSURL* url = [UIPasteboard generalPasteboard].URL;
  453. // Usually, even if the user copies plaintext, if it looks like a URL, the URL
  454. // property is filled. Sometimes, this doesn't happen, for instance when the
  455. // pasteboard is sync'd from a Mac to the iOS simulator. In this case,
  456. // fallback and manually check whether the pasteboard contains a url-like
  457. // string.
  458. if (!url) {
  459. url = [NSURL URLWithString:UIPasteboard.generalPasteboard.string];
  460. }
  461. if (![self.authorizedSchemes containsObject:url.scheme]) {
  462. return nil;
  463. }
  464. return url;
  465. }
  466. - (void)loadFromUserDefaults {
  467. self.lastPasteboardChangeCount =
  468. [self.sharedUserDefaults integerForKey:kPasteboardChangeCountKey];
  469. self.lastPasteboardChangeDate = base::mac::ObjCCastStrict<NSDate>(
  470. [self.sharedUserDefaults objectForKey:kPasteboardChangeDateKey]);
  471. }
  472. - (void)saveToUserDefaults {
  473. [self.sharedUserDefaults setInteger:self.lastPasteboardChangeCount
  474. forKey:kPasteboardChangeCountKey];
  475. [self.sharedUserDefaults setObject:self.lastPasteboardChangeDate
  476. forKey:kPasteboardChangeDateKey];
  477. }
  478. - (NSTimeInterval)uptime {
  479. return base::SysInfo::Uptime().InSecondsF();
  480. }
  481. @end