diff options
Diffstat (limited to 'chromium/components/open_from_clipboard')
15 files changed, 503 insertions, 171 deletions
diff --git a/chromium/components/open_from_clipboard/BUILD.gn b/chromium/components/open_from_clipboard/BUILD.gn index 411035c2dfd..672fa62148b 100644 --- a/chromium/components/open_from_clipboard/BUILD.gn +++ b/chromium/components/open_from_clipboard/BUILD.gn @@ -15,7 +15,6 @@ static_library("open_from_clipboard") { ] deps = [ - ":feature_flags", ":open_from_clipboard_impl", "//base", "//components/variations", @@ -39,14 +38,6 @@ static_library("open_from_clipboard") { } } -source_set("feature_flags") { - sources = [ - "clipboard_recent_content_features.cc", - "clipboard_recent_content_features.h", - ] - deps = [ "//base" ] -} - # Helper classes used by "open_from_clipboard" target. These classes must have # no dependencies on "//base:i18n". source_set("open_from_clipboard_impl") { @@ -94,7 +85,6 @@ source_set("unit_tests") { if (!is_ios) { sources += [ "clipboard_recent_content_generic_unittest.cc" ] deps += [ - ":feature_flags", "//base/test:test_support", "//ui/base/clipboard:clipboard_test_support", ] diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content.cc b/chromium/components/open_from_clipboard/clipboard_recent_content.cc index 5b2cb684719..2e59ce3afaa 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content.cc +++ b/chromium/components/open_from_clipboard/clipboard_recent_content.cc @@ -7,7 +7,6 @@ #include "base/strings/string_number_conversions.h" #include "base/time/time.h" #include "build/build_config.h" -#include "components/open_from_clipboard/clipboard_recent_content_features.h" #include "components/variations/variations_associated_data.h" #include "url/url_constants.h" @@ -35,16 +34,5 @@ void ClipboardRecentContent::SetInstance( // static base::TimeDelta ClipboardRecentContent::MaximumAgeOfClipboard() { - // Identify the current setting for this parameter from the feature, using - // 3600 seconds (1 hour) as a default if the parameter is not set. - // On iOS, the default is 600 seconds (10 minutes). - // TODO(gangwu) : Remove this feature flag after full launched in Android. -#if defined(OS_IOS) || defined(OS_ANDROID) - int default_maximum_age = 600; -#else - int default_maximum_age = 3600; -#endif - int value = variations::GetVariationParamByFeatureAsInt( - kClipboardMaximumAge, kClipboardMaximumAgeParam, default_maximum_age); - return base::TimeDelta::FromSeconds(value); + return base::TimeDelta::FromMinutes(10); } diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content.h b/chromium/components/open_from_clipboard/clipboard_recent_content.h index 5d6ed3c7b7a..6b383bda538 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content.h +++ b/chromium/components/open_from_clipboard/clipboard_recent_content.h @@ -14,6 +14,8 @@ #include "ui/gfx/image/image.h" #include "url/gurl.h" +enum class ClipboardContentType { URL, Text, Image }; + // Helper class returning an URL if the content of the clipboard can be turned // into an URL, and if it estimates that the content of the clipboard is not too // old. @@ -48,6 +50,28 @@ class ClipboardRecentContent { // Return if system's clipboard contains an image. virtual bool HasRecentImageFromClipboard() = 0; + /* + On iOS, iOS 14 introduces new clipboard APIs that are async. The asynchronous + forms of clipboard access below should be preferred. + */ + using HasDataCallback = + base::OnceCallback<void(std::set<ClipboardContentType>)>; + using GetRecentURLCallback = base::OnceCallback<void(base::Optional<GURL>)>; + using GetRecentTextCallback = + base::OnceCallback<void(base::Optional<base::string16>)>; + + // Returns whether the clipboard contains a URL to |HasDataCallback| if it + // is recent enough and has not been suppressed. + virtual void HasRecentContentFromClipboard( + std::set<ClipboardContentType> types, + HasDataCallback callback) = 0; + // Returns clipboard content as URL to |GetRecentURLCallback|, if it has a + // compatible type, is recent enough and has not been suppressed. + virtual void GetRecentURLFromClipboard(GetRecentURLCallback callback) = 0; + // Returns clipboard content as a string to |GetRecentTextCallback|, if it has + // a compatible type, is recent enough and has not been suppressed. + virtual void GetRecentTextFromClipboard(GetRecentTextCallback callback) = 0; + // Returns how old the content of the clipboard is. virtual base::TimeDelta GetClipboardContentAge() const = 0; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_features.cc b/chromium/components/open_from_clipboard/clipboard_recent_content_features.cc deleted file mode 100644 index 5082125308a..00000000000 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_features.cc +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "components/open_from_clipboard/clipboard_recent_content_features.h" - -const char kClipboardMaximumAgeParam[] = "UIClipboardMaximumAge"; - -// Feature used to determine the maximum age of clipboard content. -const base::Feature kClipboardMaximumAge{"ClipboardMaximumAge", - base::FEATURE_DISABLED_BY_DEFAULT}; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_features.h b/chromium/components/open_from_clipboard/clipboard_recent_content_features.h deleted file mode 100644 index 4b405155eee..00000000000 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_features.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef COMPONENTS_OPEN_FROM_CLIPBOARD_CLIPBOARD_RECENT_CONTENT_FEATURES_H_ -#define COMPONENTS_OPEN_FROM_CLIPBOARD_CLIPBOARD_RECENT_CONTENT_FEATURES_H_ - -#include "base/feature_list.h" - -// Parameter name for the ClipboardMaximumAge experiment. -extern const char kClipboardMaximumAgeParam[]; - -extern const base::Feature kClipboardMaximumAge; - -#endif // COMPONENTS_OPEN_FROM_CLIPBOARD_CLIPBOARD_RECENT_CONTENT_FEATURES_H_ diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_generic.cc b/chromium/components/open_from_clipboard/clipboard_recent_content_generic.cc index 9d3f259696d..f3d94668bdf 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_generic.cc +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_generic.cc @@ -114,6 +114,42 @@ bool ClipboardRecentContentGeneric::HasRecentImageFromClipboard() { ui::ClipboardBuffer::kCopyPaste); } +void ClipboardRecentContentGeneric::HasRecentContentFromClipboard( + std::set<ClipboardContentType> types, + HasDataCallback callback) { + std::set<ClipboardContentType> matching_types; + for (ClipboardContentType type : types) { + switch (type) { + case ClipboardContentType::URL: + if (GetRecentURLFromClipboard()) { + matching_types.insert(ClipboardContentType::URL); + } + break; + case ClipboardContentType::Text: + if (GetRecentTextFromClipboard()) { + matching_types.insert(ClipboardContentType::Text); + } + break; + case ClipboardContentType::Image: + if (HasRecentImageFromClipboard()) { + matching_types.insert(ClipboardContentType::Image); + } + break; + } + } + std::move(callback).Run(matching_types); +} + +void ClipboardRecentContentGeneric::GetRecentURLFromClipboard( + GetRecentURLCallback callback) { + std::move(callback).Run(GetRecentURLFromClipboard()); +} + +void ClipboardRecentContentGeneric::GetRecentTextFromClipboard( + GetRecentTextCallback callback) { + std::move(callback).Run(GetRecentTextFromClipboard()); +} + base::TimeDelta ClipboardRecentContentGeneric::GetClipboardContentAge() const { const base::Time last_modified_time = ui::Clipboard::GetForCurrentThread()->GetLastModifiedTime(); diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_generic.h b/chromium/components/open_from_clipboard/clipboard_recent_content_generic.h index 218a411a093..2a4d2df24a9 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_generic.h +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_generic.h @@ -28,6 +28,10 @@ class ClipboardRecentContentGeneric : public ClipboardRecentContent { base::Optional<base::string16> GetRecentTextFromClipboard() override; void GetRecentImageFromClipboard(GetRecentImageCallback callback) override; bool HasRecentImageFromClipboard() override; + void HasRecentContentFromClipboard(std::set<ClipboardContentType> types, + HasDataCallback callback) override; + void GetRecentURLFromClipboard(GetRecentURLCallback callback) override; + void GetRecentTextFromClipboard(GetRecentTextCallback callback) override; base::TimeDelta GetClipboardContentAge() const override; void SuppressClipboardContent() override; void ClearClipboardContent() override; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_generic_unittest.cc b/chromium/components/open_from_clipboard/clipboard_recent_content_generic_unittest.cc index bc9cffaa065..c7c81d47ce0 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_generic_unittest.cc +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_generic_unittest.cc @@ -13,7 +13,6 @@ #include "base/strings/utf_string_conversions.h" #include "base/test/scoped_feature_list.h" #include "base/time/time.h" -#include "components/open_from_clipboard/clipboard_recent_content_features.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/clipboard/test/test_clipboard.h" #include "url/gurl.h" @@ -77,9 +76,6 @@ TEST_F(ClipboardRecentContentGenericTest, RecognizesURLs) { } TEST_F(ClipboardRecentContentGenericTest, OlderURLsNotSuggested) { - base::test::ScopedFeatureList scoped_feature_list; - scoped_feature_list.InitAndEnableFeatureWithParameters( - kClipboardMaximumAge, {{kClipboardMaximumAgeParam, "600"}}); ClipboardRecentContentGeneric recent_content; base::Time now = base::Time::Now(); std::string text = "http://example.com/"; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.h b/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.h index 19bbf849be4..38f708b72d9 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.h +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.h @@ -8,6 +8,12 @@ #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> +typedef NSString* ContentType NS_TYPED_ENUM; + +extern ContentType const ContentTypeURL; +extern ContentType const ContentTypeText; +extern ContentType const ContentTypeImage; + // A protocol implemented by delegates to handle clipboard changes. @protocol ClipboardRecentContentDelegate<NSObject> @@ -44,6 +50,25 @@ // not been suppressed. Otherwise, returns nil. - (UIImage*)recentImageFromClipboard; +// Uses the new iOS 14 pasteboard detection pattern API to asynchronously detect +// if the clipboard contains content (that has not been suppressed) of the +// requested types without actually getting the contents. +- (void)hasContentMatchingTypes:(NSSet<ContentType>*)types + completionHandler: + (void (^)(NSSet<ContentType>*))completionHandler; +// Uses the new iOS 14 pasteboard detection pattern API to asynchronously get a +// copied URL from the clipboard if it has not been suppressed. Passes nil to +// the callback otherwise. +- (void)recentURLFromClipboardAsync:(void (^)(NSURL*))callback; +// Uses the new iOS 14 pasteboard detection pattern API to asynchronously get a +// copied string from the clipboard if it has not been suppressed. Passes nil to +// the callback otherwise. +- (void)recentTextFromClipboardAsync:(void (^)(NSString*))callback; +// Asynchronously gets an image from the clipboard if is has not been +// suppressed. Passes nil to the callback otherwise. This does not actually use +// any iOS 14 APIs and could be done synchronously, but is here for consistency. +- (void)recentImageFromClipboardAsync:(void (^)(UIImage*))callback; + // Returns how old the content of the clipboard is. - (NSTimeInterval)clipboardContentAge; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.mm b/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.mm index a568745bb02..20b1ce342d4 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.mm +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_impl_ios.mm @@ -4,7 +4,6 @@ #import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h" -#import <CommonCrypto/CommonDigest.h> #import <MobileCoreServices/MobileCoreServices.h> #import <UIKit/UIKit.h> @@ -16,6 +15,10 @@ #error "This file requires ARC support." #endif +ContentType const ContentTypeURL = @"ContentTypeURL"; +ContentType const ContentTypeText = @"ContentTypeString"; +ContentType const ContentTypeImage = @"ContentTypeImage"; + namespace { // Key used to store the pasteboard's current change count. If when resuming // chrome the pasteboard's change count is different from the stored one, then @@ -24,45 +27,6 @@ NSString* const kPasteboardChangeCountKey = @"PasteboardChangeCount"; // Key used to store the last date at which it was detected that the pasteboard // changed. It is used to evaluate the age of the pasteboard's content. NSString* const kPasteboardChangeDateKey = @"PasteboardChangeDate"; -// Key used to store the hash of the content of the pasteboard. Whenever the -// hash changed, the pasteboard content is considered to have changed. -NSString* const kPasteboardEntryMD5Key = @"PasteboardEntryMD5"; - -// Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|, -// |image_data|, and |url|. This value is used to detect pasteboard content -// change. Keeping only 4 bytes is a privacy requirement to introduce collision -// and allow deniability of having copied a given string, image, or url. -// -// |image_data| is passed in as NSData instead of UIImage because converting -// UIImage to NSData can be slow for large images and getting NSData directly -// from the pasteboard is quicker. -NSData* WeakMD5FromPasteboardData(NSString* string, - NSData* image_data, - NSURL* url) { - CC_MD5_CTX ctx; - CC_MD5_Init(&ctx); - - const std::string clipboard_string = base::SysNSStringToUTF8(string); - const char* c_string = clipboard_string.c_str(); - CC_MD5_Update(&ctx, c_string, strlen(c_string)); - - // This hash is used only to tell if the image has changed, so - // limit the number of bytes to hash to prevent slowdown. - NSUInteger bytes_to_hash = fmin([image_data length], 1000000); - if (bytes_to_hash > 0) { - CC_MD5_Update(&ctx, [image_data bytes], bytes_to_hash); - } - - const std::string url_string = base::SysNSStringToUTF8([url absoluteString]); - const char* url_c_string = url_string.c_str(); - CC_MD5_Update(&ctx, url_c_string, strlen(url_c_string)); - - unsigned char hash[CC_MD5_DIGEST_LENGTH]; - CC_MD5_Final(hash, &ctx); - - NSData* data = [NSData dataWithBytes:hash length:4]; - return data; -} } // namespace @@ -73,8 +37,6 @@ NSData* WeakMD5FromPasteboardData(NSString* string, @property(nonatomic, strong) NSUserDefaults* sharedUserDefaults; // The pasteboard's change count. Increases everytime the pasteboard changes. @property(nonatomic) NSInteger lastPasteboardChangeCount; -// MD5 hash of the last registered pasteboard entry. -@property(nonatomic, strong) NSData* lastPasteboardEntryMD5; // Contains the authorized schemes for URLs. @property(nonatomic, readonly) NSSet* authorizedSchemes; // Delegate for metrics. @@ -82,8 +44,18 @@ NSData* WeakMD5FromPasteboardData(NSString* string, // Maximum age of clipboard in seconds. @property(nonatomic, readonly) NSTimeInterval maximumAgeOfClipboard; -// If the content of the pasteboard has changed, updates the change count, -// change date, and md5 of the latest pasteboard entry if necessary. +// A cached version of an already-retrieved URL. This prevents subsequent URL +// requests from triggering the iOS 14 pasteboard notification. +@property(nonatomic, strong) NSURL* cachedURL; +// A cached version of an already-retrieved string. This prevents subsequent +// string requests from triggering the iOS 14 pasteboard notification. +@property(nonatomic, copy) NSString* cachedText; +// A cached version of an already-retrieved image. This prevents subsequent +// image requests from triggering the iOS 14 pasteboard notification. +@property(nonatomic, strong) UIImage* cachedImage; + +// If the content of the pasteboard has changed, updates the change count +// and change date. - (void)updateIfNeeded; // Returns whether the pasteboard changed since the last time a pasteboard @@ -99,13 +71,15 @@ NSData* WeakMD5FromPasteboardData(NSString* string, // Returns the uptime. - (NSTimeInterval)uptime; +// Returns whether the value of the clipboard should be returned. +- (BOOL)shouldReturnValueOfClipboard; + @end @implementation ClipboardRecentContentImplIOS @synthesize lastPasteboardChangeCount = _lastPasteboardChangeCount; @synthesize lastPasteboardChangeDate = _lastPasteboardChangeDate; -@synthesize lastPasteboardEntryMD5 = _lastPasteboardEntryMD5; @synthesize sharedUserDefaults = _sharedUserDefaults; @synthesize authorizedSchemes = _authorizedSchemes; @synthesize delegate = _delegate; @@ -146,93 +120,282 @@ NSData* WeakMD5FromPasteboardData(NSString* string, [self updateIfNeeded]; } -- (NSData*)getCurrentMD5 { - NSString* pasteboardString = [UIPasteboard generalPasteboard].string; - NSData* pasteboardImageData = [[UIPasteboard generalPasteboard] - dataForPasteboardType:(NSString*)kUTTypeImage]; - NSURL* pasteboardURL = [UIPasteboard generalPasteboard].URL; - NSData* md5 = WeakMD5FromPasteboardData(pasteboardString, pasteboardImageData, - pasteboardURL); +- (BOOL)hasPasteboardChanged { + return UIPasteboard.generalPasteboard.changeCount != + self.lastPasteboardChangeCount; +} + +- (NSURL*)recentURLFromClipboard { + [self updateIfNeeded]; + + if (![self shouldReturnValueOfClipboard]) + return nil; - return md5; + if (!self.cachedURL) { + self.cachedURL = [self URLFromPasteboard]; + } + return self.cachedURL; } -- (BOOL)hasPasteboardChanged { - // If |MD5Changed|, we know for sure there has been at least one pasteboard - // copy since last time it was checked. - // If the pasteboard content is still the same but the device was not - // rebooted, the change count can be checked to see if it changed. - // Note: due to a mismatch between the actual behavior and documentation, and - // lack of consistency on different reboot scenarios, the change count cannot - // be checked after a reboot. - // See radar://21833556 for more information. - BOOL deviceRebooted = [self clipboardContentAge] >= [self uptime]; - - // On iOS 13, there is a bug where every time a UITextField is opened, the - // changeCount increases by 2. Thus, if the difference in counts is even, - // it is unknown whether there is a real change or the user just focused some - // UITextFields. - // See radar://7619972 or crbug.com/1058487 for more information. - NSInteger changeCount = [UIPasteboard generalPasteboard].changeCount; - BOOL changeCountChanged = changeCount != self.lastPasteboardChangeCount; - if (@available(iOS 13, *)) { - // No-op. This should be if !available(13), but that is not supported. - } else { - if (!deviceRebooted) { - return changeCountChanged; +- (NSString*)recentTextFromClipboard { + [self updateIfNeeded]; + + if (![self shouldReturnValueOfClipboard]) + return nil; + + if (!self.cachedText) { + self.cachedText = UIPasteboard.generalPasteboard.string; + } + return self.cachedText; +} + +- (UIImage*)recentImageFromClipboard { + [self updateIfNeeded]; + + if (![self shouldReturnValueOfClipboard]) + return nil; + + if (!self.cachedImage) { + self.cachedImage = UIPasteboard.generalPasteboard.image; + } + + return self.cachedImage; +} + +- (void)hasContentMatchingTypes:(NSSet<ContentType>*)types + completionHandler: + (void (^)(NSSet<ContentType>*))completionHandler { + [self updateIfNeeded]; + if (![self shouldReturnValueOfClipboard]) { + completionHandler([NSSet set]); + return; + } + + __block NSMutableDictionary<ContentType, NSNumber*>* results = + [[NSMutableDictionary alloc] init]; + + void (^checkResults)() = ^{ + NSMutableSet<ContentType>* matchingTypes = [NSMutableSet set]; + if ([results count] != [types count]) { + return; + } + + for (ContentType type in results) { + if ([results[type] boolValue]) { + [matchingTypes addObject:type]; + } + } + completionHandler(matchingTypes); + }; + + for (ContentType type in types) { + if ([type isEqualToString:ContentTypeURL]) { + [self hasRecentURLFromClipboardInternal:^(BOOL hasURL) { + results[ContentTypeURL] = [NSNumber numberWithBool:hasURL]; + checkResults(); + }]; + } else if ([type isEqualToString:ContentTypeText]) { + [self hasRecentTextFromClipboardInternal:^(BOOL hasText) { + results[ContentTypeText] = [NSNumber numberWithBool:hasText]; + checkResults(); + }]; + } else if ([type isEqualToString:ContentTypeImage]) { + [self hasRecentImageFromClipboardInternal:^(BOOL hasImage) { + results[ContentTypeImage] = [NSNumber numberWithBool:hasImage]; + checkResults(); + }]; } } +} - // If there was no reboot, and the number hasn't changed, the pasteboard - // definitely hasn't changed. - if (!deviceRebooted && !changeCountChanged) { - return NO; +- (void)hasRecentURLFromClipboardInternal:(void (^)(BOOL))callback { + DCHECK(callback); + if (@available(iOS 14, *)) { + // Use cached value if it exists + if (self.cachedURL) { + callback(YES); + return; + } + +#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + NSSet<UIPasteboardDetectionPattern>* urlPattern = + [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebURL]; + [UIPasteboard.generalPasteboard + detectPatternsForPatterns:urlPattern + completionHandler:^( + NSSet<UIPasteboardDetectionPattern>* patterns, + NSError* error) { + callback([patterns + containsObject: + UIPasteboardDetectionPatternProbableWebURL]); + }]; +#else + // To prevent clipboard notification from appearing on iOS 14 with iOS 13 + // SDK, use the -hasURLs property to check for URL existence. This will + // cause crbug.com/1033935 to reappear in code using this method (also see + // the comments in -URLFromPasteboard in this file), but that is preferable + // to the notificatio appearing when it shouldn't. + callback(UIPasteboard.generalPasteboard.hasURLs); +#endif + } else { + callback([self recentURLFromClipboard] != nil); } +} + +- (void)hasRecentTextFromClipboardInternal:(void (^)(BOOL))callback { + DCHECK(callback); + if (@available(iOS 14, *)) { + // Use cached value if it exists + if (self.cachedText) { + callback(YES); + return; + } - // If there was no reboot and the size of the change is odd , the pasteboard - // definitely has changed. - BOOL changeCountIncreasedIsOdd = - (changeCount - self.lastPasteboardChangeCount) % 2 != 0; - if (!deviceRebooted && changeCountIncreasedIsOdd) { - return YES; +#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + NSSet<UIPasteboardDetectionPattern>* textPattern = + [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebSearch]; + [UIPasteboard.generalPasteboard + detectPatternsForPatterns:textPattern + completionHandler:^( + NSSet<UIPasteboardDetectionPattern>* patterns, + NSError* error) { + callback([patterns + containsObject: + UIPasteboardDetectionPatternProbableWebSearch]); + }]; +#else + callback(UIPasteboard.generalPasteboard.hasStrings); +#endif + } else { + callback([self recentTextFromClipboard] != nil); } +} + +- (void)hasRecentImageFromClipboardInternal:(void (^)(BOOL))callback { + DCHECK(callback); + if (@available(iOS 14, *)) { + // Use cached value if it exists + if (self.cachedImage) { + callback(YES); + return; + } - // Otherwise, it is unknown whether or not there was a real change, so - // fallback to looking at the MD5. - self.lastPasteboardChangeCount = changeCount; - BOOL md5Changed = - ![[self getCurrentMD5] isEqualToData:self.lastPasteboardEntryMD5]; - return md5Changed; + callback(UIPasteboard.generalPasteboard.hasImages); + } else { + callback([self recentImageFromClipboard] != nil); + } } -- (NSURL*)recentURLFromClipboard { - [self updateIfNeeded]; - if ([self clipboardContentAge] > self.maximumAgeOfClipboard) { - return nil; +- (void)recentURLFromClipboardAsync:(void (^)(NSURL*))callback { + DCHECK(callback); + if (@available(iOS 14, *)) { + [self updateIfNeeded]; + if (![self shouldReturnValueOfClipboard]) { + callback(nil); + return; + } + + // Use cached value if it exists. + if (self.cachedURL) { + callback(self.cachedURL); + return; + } + +#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + __weak __typeof(self) weakSelf = self; + NSSet<UIPasteboardDetectionPattern>* urlPattern = + [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebURL]; + [UIPasteboard.generalPasteboard + detectValuesForPatterns:urlPattern + completionHandler:^( + NSDictionary<UIPasteboardDetectionPattern, id>* values, + NSError* error) { + NSURL* url = [NSURL + URLWithString: + values[UIPasteboardDetectionPatternProbableWebURL]]; + weakSelf.cachedURL = url; + callback(url); + }]; +#else + callback([self recentURLFromClipboard]); +#endif + } else { + callback([self recentURLFromClipboard]); } - return [self URLFromPasteboard]; } -- (NSString*)recentTextFromClipboard { - [self updateIfNeeded]; - if ([self clipboardContentAge] > self.maximumAgeOfClipboard) { - return nil; +- (void)recentTextFromClipboardAsync:(void (^)(NSString*))callback { + DCHECK(callback); + if (@available(iOS 14, *)) { + [self updateIfNeeded]; + if (![self shouldReturnValueOfClipboard]) { + callback(nil); + return; + } + + // Use cached value if it exists. + if (self.cachedText) { + callback(self.cachedText); + return; + } + +#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + __weak __typeof(self) weakSelf = self; + NSSet<UIPasteboardDetectionPattern>* textPattern = + [NSSet setWithObject:UIPasteboardDetectionPatternProbableWebSearch]; + [UIPasteboard.generalPasteboard + detectValuesForPatterns:textPattern + completionHandler:^( + NSDictionary<UIPasteboardDetectionPattern, id>* values, + NSError* error) { + NSString* text = + values[UIPasteboardDetectionPatternProbableWebSearch]; + weakSelf.cachedText = text; + + callback(text); + }]; +#else + callback([self recentTextFromClipboard]); +#endif + } else { + callback([self recentTextFromClipboard]); } - return [UIPasteboard generalPasteboard].string; } -- (UIImage*)recentImageFromClipboard { +- (void)recentImageFromClipboardAsync:(void (^)(UIImage*))callback { + DCHECK(callback); [self updateIfNeeded]; - if ([self clipboardContentAge] > self.maximumAgeOfClipboard) { - return nil; + if (![self shouldReturnValueOfClipboard]) { + callback(nil); + return; + } + + if (!self.cachedImage) { + self.cachedImage = UIPasteboard.generalPasteboard.image; } - return [UIPasteboard generalPasteboard].image; + callback(self.cachedImage); } - (NSTimeInterval)clipboardContentAge { return -[self.lastPasteboardChangeDate timeIntervalSinceNow]; } +- (BOOL)shouldReturnValueOfClipboard { + if ([self clipboardContentAge] > self.maximumAgeOfClipboard) + return NO; + + // It is the common convention on iOS that password managers tag confidential + // data with the flavor "org.nspasteboard.ConcealedType". Obey this + // convention; the user doesn't want for their confidential data to be + // suggested as a search, anyway. See http://nspasteboard.org/ for more info. + NSArray<NSString*>* types = + [[UIPasteboard generalPasteboard] pasteboardTypes]; + if ([types containsObject:@"org.nspasteboard.ConcealedType"]) + return NO; + + return YES; +} + - (void)suppressClipboardContent { // User cleared the user data. The pasteboard entry must be removed from the // omnibox list. Force entry expiration by setting copy date to 1970. @@ -246,11 +409,15 @@ NSData* WeakMD5FromPasteboardData(NSString* string, return; } - [self.delegate onClipboardChanged]; - self.lastPasteboardChangeDate = [NSDate date]; self.lastPasteboardChangeCount = [UIPasteboard generalPasteboard].changeCount; - self.lastPasteboardEntryMD5 = [self getCurrentMD5]; + + // Clear the cache because the pasteboard data has changed. + self.cachedURL = nil; + self.cachedText = nil; + self.cachedImage = nil; + + [self.delegate onClipboardChanged]; [self saveToUserDefaults]; } @@ -276,8 +443,6 @@ NSData* WeakMD5FromPasteboardData(NSString* string, [self.sharedUserDefaults integerForKey:kPasteboardChangeCountKey]; self.lastPasteboardChangeDate = base::mac::ObjCCastStrict<NSDate>( [self.sharedUserDefaults objectForKey:kPasteboardChangeDateKey]); - self.lastPasteboardEntryMD5 = base::mac::ObjCCastStrict<NSData>( - [self.sharedUserDefaults objectForKey:kPasteboardEntryMD5Key]); } - (void)saveToUserDefaults { @@ -285,8 +450,6 @@ NSData* WeakMD5FromPasteboardData(NSString* string, forKey:kPasteboardChangeCountKey]; [self.sharedUserDefaults setObject:self.lastPasteboardChangeDate forKey:kPasteboardChangeDateKey]; - [self.sharedUserDefaults setObject:self.lastPasteboardEntryMD5 - forKey:kPasteboardEntryMD5Key]; } - (NSTimeInterval)uptime { diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_ios.h b/chromium/components/open_from_clipboard/clipboard_recent_content_ios.h index 63b3c407160..e264db8721f 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_ios.h +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_ios.h @@ -44,6 +44,10 @@ class ClipboardRecentContentIOS : public ClipboardRecentContent { base::Optional<base::string16> GetRecentTextFromClipboard() override; void GetRecentImageFromClipboard(GetRecentImageCallback callback) override; bool HasRecentImageFromClipboard() override; + void HasRecentContentFromClipboard(std::set<ClipboardContentType> types, + HasDataCallback callback) override; + void GetRecentURLFromClipboard(GetRecentURLCallback callback) override; + void GetRecentTextFromClipboard(GetRecentTextCallback callback) override; base::TimeDelta GetClipboardContentAge() const override; void SuppressClipboardContent() override; void ClearClipboardContent() override; diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_ios.mm b/chromium/components/open_from_clipboard/clipboard_recent_content_ios.mm index 5287c1c193e..3bec1c01602 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_ios.mm +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_ios.mm @@ -44,6 +44,29 @@ NSSet<NSString*>* getAuthorizedSchemeList( return [schemes copy]; } +ContentType ContentTypeFromClipboardContentType(ClipboardContentType type) { + switch (type) { + case ClipboardContentType::URL: + return ContentTypeURL; + case ClipboardContentType::Text: + return ContentTypeText; + case ClipboardContentType::Image: + return ContentTypeImage; + } +} + +ClipboardContentType ClipboardContentTypeFromContentType(ContentType type) { + if ([type isEqualToString:ContentTypeURL]) { + return ClipboardContentType::URL; + } else if ([type isEqualToString:ContentTypeText]) { + return ClipboardContentType::Text; + } else if ([type isEqualToString:ContentTypeImage]) { + return ClipboardContentType::Image; + } + NOTREACHED(); + return ClipboardContentType::Text; +} + } // namespace @interface ClipboardRecentContentDelegateImpl @@ -95,13 +118,65 @@ ClipboardRecentContentIOS::GetRecentTextFromClipboard() { void ClipboardRecentContentIOS::GetRecentImageFromClipboard( GetRecentImageCallback callback) { - std::move(callback).Run(GetRecentImageFromClipboardInternal()); + __block GetRecentImageCallback callback_for_block = std::move(callback); + [implementation_ recentImageFromClipboardAsync:^(UIImage* image) { + if (!image) { + std::move(callback_for_block).Run(base::nullopt); + return; + } + + std::move(callback_for_block).Run(gfx::Image(image)); + }]; } bool ClipboardRecentContentIOS::HasRecentImageFromClipboard() { return GetRecentImageFromClipboardInternal().has_value(); } +void ClipboardRecentContentIOS::HasRecentContentFromClipboard( + std::set<ClipboardContentType> types, + HasDataCallback callback) { + __block HasDataCallback callback_for_block = std::move(callback); + NSMutableSet<ContentType>* ios_types = [NSMutableSet set]; + for (ClipboardContentType type : types) { + [ios_types addObject:ContentTypeFromClipboardContentType(type)]; + } + [implementation_ hasContentMatchingTypes:ios_types + completionHandler:^(NSSet<ContentType>* results) { + std::set<ClipboardContentType> matching_types; + for (ContentType type in results) { + matching_types.insert( + ClipboardContentTypeFromContentType(type)); + } + std::move(callback_for_block).Run(matching_types); + }]; +} + +void ClipboardRecentContentIOS::GetRecentURLFromClipboard( + GetRecentURLCallback callback) { + __block GetRecentURLCallback callback_for_block = std::move(callback); + [implementation_ recentURLFromClipboardAsync:^(NSURL* url) { + GURL converted_url = net::GURLWithNSURL(url); + if (!converted_url.is_valid()) { + std::move(callback_for_block).Run(base::nullopt); + return; + } + std::move(callback_for_block).Run(converted_url); + }]; +} + +void ClipboardRecentContentIOS::GetRecentTextFromClipboard( + GetRecentTextCallback callback) { + __block GetRecentTextCallback callback_for_block = std::move(callback); + [implementation_ recentTextFromClipboardAsync:^(NSString* text) { + if (!text) { + std::move(callback_for_block).Run(base::nullopt); + return; + } + std::move(callback_for_block).Run(base::SysNSStringToUTF16(text)); + }]; +} + ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {} base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { diff --git a/chromium/components/open_from_clipboard/clipboard_recent_content_ios_unittest.mm b/chromium/components/open_from_clipboard/clipboard_recent_content_ios_unittest.mm index 349ba4b1d7f..d56ba5b99e0 100644 --- a/chromium/components/open_from_clipboard/clipboard_recent_content_ios_unittest.mm +++ b/chromium/components/open_from_clipboard/clipboard_recent_content_ios_unittest.mm @@ -217,9 +217,22 @@ TEST_F(ClipboardRecentContentIOSTest, PasteboardURLObsolescence) { VerifyClipboardTextDoesNotExist(); } +// Checks that if the pasteboard is marked as having confidential data, it is +// not returned. +TEST_F(ClipboardRecentContentIOSTest, ConfidentialPasteboardText) { + [[UIPasteboard generalPasteboard] + setItems:@[ @{ + @"public.plain-text" : @"hunter2", + @"org.nspasteboard.ConcealedType" : @"hunter2" + } ] + options:@{}]; + + VerifyClipboardTextDoesNotExist(); +} + // Checks that if the user suppresses content, no text will be returned, // and if the text changes, the new text will be returned again. -TEST_F(ClipboardRecentContentIOSTest, SupressedPasteboardText) { +TEST_F(ClipboardRecentContentIOSTest, SuppressedPasteboardText) { SetPasteboardContent(kRecognizedURL); // Test that recent pasteboard data is provided. @@ -248,7 +261,7 @@ TEST_F(ClipboardRecentContentIOSTest, SupressedPasteboardText) { VerifyClipboardTextDoesNotExist(); // Check that if the pasteboard changes, the new content is not - // supressed anymore. + // suppressed anymore. SetPasteboardContent(kRecognizedURL2); VerifyClipboardURLExists(kRecognizedURL2); VerifyClipboardTextExists(kRecognizedURL2); @@ -256,7 +269,7 @@ TEST_F(ClipboardRecentContentIOSTest, SupressedPasteboardText) { // Checks that if the user suppresses content, no image will be returned, // and if the image changes, the new image will be returned again. -TEST_F(ClipboardRecentContentIOSTest, SupressedPasteboardImage) { +TEST_F(ClipboardRecentContentIOSTest, SuppressedPasteboardImage) { SetPasteboardImage(TestUIImage()); // Test that recent pasteboard data is provided. @@ -281,7 +294,7 @@ TEST_F(ClipboardRecentContentIOSTest, SupressedPasteboardImage) { VerifyIfClipboardImageExists(false); // Check that if the pasteboard changes, the new content is not - // supressed anymore. + // suppressed anymore. SetPasteboardImage(TestUIImage([UIColor greenColor])); VerifyIfClipboardImageExists(true); } diff --git a/chromium/components/open_from_clipboard/fake_clipboard_recent_content.cc b/chromium/components/open_from_clipboard/fake_clipboard_recent_content.cc index 2ee12524db1..6fcc12cdadc 100644 --- a/chromium/components/open_from_clipboard/fake_clipboard_recent_content.cc +++ b/chromium/components/open_from_clipboard/fake_clipboard_recent_content.cc @@ -40,6 +40,42 @@ bool FakeClipboardRecentContent::HasRecentImageFromClipboard() { return clipboard_image_content_.has_value(); } +void FakeClipboardRecentContent::HasRecentContentFromClipboard( + std::set<ClipboardContentType> types, + HasDataCallback callback) { + std::set<ClipboardContentType> matching_types; + for (ClipboardContentType type : types) { + switch (type) { + case ClipboardContentType::URL: + if (GetRecentURLFromClipboard()) { + matching_types.insert(ClipboardContentType::URL); + } + break; + case ClipboardContentType::Text: + if (GetRecentTextFromClipboard()) { + matching_types.insert(ClipboardContentType::Text); + } + break; + case ClipboardContentType::Image: + if (HasRecentImageFromClipboard()) { + matching_types.insert(ClipboardContentType::Image); + } + break; + } + } + std::move(callback).Run(matching_types); +} + +void FakeClipboardRecentContent::GetRecentURLFromClipboard( + GetRecentURLCallback callback) { + std::move(callback).Run(GetRecentURLFromClipboard()); +} + +void FakeClipboardRecentContent::GetRecentTextFromClipboard( + GetRecentTextCallback callback) { + std::move(callback).Run(GetRecentTextFromClipboard()); +} + base::TimeDelta FakeClipboardRecentContent::GetClipboardContentAge() const { return content_age_; } diff --git a/chromium/components/open_from_clipboard/fake_clipboard_recent_content.h b/chromium/components/open_from_clipboard/fake_clipboard_recent_content.h index 8906925bc7b..b2d36ead079 100644 --- a/chromium/components/open_from_clipboard/fake_clipboard_recent_content.h +++ b/chromium/components/open_from_clipboard/fake_clipboard_recent_content.h @@ -23,6 +23,10 @@ class FakeClipboardRecentContent : public ClipboardRecentContent { base::Optional<base::string16> GetRecentTextFromClipboard() override; void GetRecentImageFromClipboard(GetRecentImageCallback callback) override; bool HasRecentImageFromClipboard() override; + void HasRecentContentFromClipboard(std::set<ClipboardContentType> types, + HasDataCallback callback) override; + void GetRecentURLFromClipboard(GetRecentURLCallback callback) override; + void GetRecentTextFromClipboard(GetRecentTextCallback callback) override; base::TimeDelta GetClipboardContentAge() const override; void SuppressClipboardContent() override; void ClearClipboardContent() override; |