Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h" | 5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h" |
| 6 | 6 |
| 7 #import <CommonCrypto/CommonDigest.h> | 7 #import <CommonCrypto/CommonDigest.h> |
| 8 #include <stddef.h> | 8 #include <stddef.h> |
| 9 #include <stdint.h> | 9 #include <stdint.h> |
| 10 #import <UIKit/UIKit.h> | 10 #import <UIKit/UIKit.h> |
| 11 | 11 |
| 12 #import "base/ios/ios_util.h" | 12 #import "base/ios/ios_util.h" |
| 13 #include "base/logging.h" | 13 #include "base/logging.h" |
| 14 #include "base/macros.h" | 14 #include "base/macros.h" |
| 15 #include "base/metrics/user_metrics.h" | 15 #include "base/metrics/user_metrics.h" |
| 16 #include "base/strings/sys_string_conversions.h" | 16 #include "base/strings/sys_string_conversions.h" |
| 17 #include "base/sys_info.h" | 17 #include "base/sys_info.h" |
| 18 #import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h" | |
| 19 #import "net/base/mac/url_conversions.h" | |
| 18 #include "url/gurl.h" | 20 #include "url/gurl.h" |
| 19 #include "url/url_constants.h" | 21 #include "url/url_constants.h" |
| 20 | 22 |
| 21 // Bridge that forwards UIApplicationDidBecomeActiveNotification notifications | 23 namespace { |
| 22 // to its delegate. | 24 // Schemes accepted by the ClipboardRecentContentIOS. |
| 23 @interface ApplicationDidBecomeActiveNotificationListenerBridge : NSObject | 25 const char* kAuthorizedSchemes[] = { |
| 26 url::kHttpScheme, url::kHttpsScheme, url::kDataScheme, url::kAboutScheme, | |
| 27 }; | |
| 24 | 28 |
| 25 // Initialize the ApplicationDidBecomeActiveNotificationListenerBridge with | 29 // Get the list of authorized schemes. |
| 26 // |delegate| which must not be null. | 30 NSSet* getAuthorizedSchemeList(const std::string& application_scheme) { |
|
marq (ping after 24h)
2017/04/05 07:58:10
NSSet<NSString*>*
lody
2017/04/06 11:27:22
Done.
| |
| 27 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate | 31 NSMutableSet* schemes = [NSMutableSet set]; |
|
marq (ping after 24h)
2017/04/05 07:58:10
NSMutableSet<NSString*>*
lody
2017/04/06 11:27:22
Done.
| |
| 28 NS_DESIGNATED_INITIALIZER; | 32 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { |
| 33 [schemes addObject:base::SysUTF8ToNSString(kAuthorizedSchemes[i])]; | |
| 34 } | |
| 35 if (!application_scheme.empty()) { | |
| 36 [schemes addObject:base::SysUTF8ToNSString(application_scheme)]; | |
| 37 } | |
| 29 | 38 |
| 30 - (instancetype)init NS_UNAVAILABLE; | 39 return schemes; |
|
marq (ping after 24h)
2017/04/05 07:58:10
nit: return [schemes copy] so the return value isn
lody
2017/04/06 11:27:22
Done.
| |
| 40 } | |
| 41 } | |
| 31 | 42 |
| 43 @interface ClipboardRecentContentDelegateImpl | |
| 44 : NSObject<ClipboardRecentContentDelegate> | |
| 32 @end | 45 @end |
| 33 | 46 |
| 34 @implementation ApplicationDidBecomeActiveNotificationListenerBridge { | 47 @implementation ClipboardRecentContentDelegateImpl |
| 35 ClipboardRecentContentIOS* _delegate; | |
| 36 } | |
| 37 | 48 |
| 38 - (instancetype)init { | 49 - (void)onClipboardChanged { |
| 39 NOTREACHED(); | 50 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
| 40 return nil; | |
| 41 } | |
| 42 | |
| 43 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate { | |
| 44 DCHECK(delegate); | |
| 45 self = [super init]; | |
| 46 if (self) { | |
| 47 _delegate = delegate; | |
| 48 [[NSNotificationCenter defaultCenter] | |
| 49 addObserver:self | |
| 50 selector:@selector(didBecomeActive:) | |
| 51 name:UIApplicationDidBecomeActiveNotification | |
| 52 object:nil]; | |
| 53 } | |
| 54 return self; | |
| 55 } | |
| 56 | |
| 57 - (void)dealloc { | |
| 58 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| 59 [super dealloc]; | |
| 60 } | |
| 61 | |
| 62 - (void)didBecomeActive:(NSNotification*)notification { | |
| 63 if (_delegate) { | |
| 64 _delegate->LoadFromUserDefaults(); | |
| 65 _delegate->UpdateIfNeeded(); | |
| 66 } | |
| 67 } | |
| 68 | |
| 69 - (void)disconnect { | |
| 70 _delegate = nullptr; | |
| 71 } | 51 } |
| 72 | 52 |
| 73 @end | 53 @end |
| 74 | 54 |
| 75 namespace { | 55 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| 76 // Key used to store the pasteboard's current change count. If when resuming | 56 const std::string& application_scheme, |
| 77 // chrome the pasteboard's change count is different from the stored one, then | 57 NSUserDefaults* group_user_defaults) |
| 78 // it means that the pasteboard's content has changed. | 58 : ClipboardRecentContentIOS([[ClipboardRecentContentImplIOS alloc] |
| 79 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; | 59 initWithAuthorizedSchemes:getAuthorizedSchemeList(application_scheme) |
| 80 // Key used to store the last date at which it was detected that the pasteboard | 60 userDefaults:group_user_defaults |
| 81 // changed. It is used to evaluate the age of the pasteboard's content. | 61 delegate:[[ClipboardRecentContentDelegateImpl alloc] |
| 82 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; | 62 init]]) {} |
| 83 // Key used to store the hash of the content of the pasteboard. Whenever the | |
| 84 // hash changed, the pasteboard content is considered to have changed. | |
| 85 NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5"; | |
| 86 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); | |
| 87 // Schemes accepted by the ClipboardRecentContentIOS. | |
| 88 const char* kAuthorizedSchemes[] = { | |
| 89 url::kHttpScheme, | |
| 90 url::kHttpsScheme, | |
| 91 url::kDataScheme, | |
| 92 url::kAboutScheme, | |
| 93 }; | |
| 94 | 63 |
| 95 // Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|. | 64 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| 96 // This value is used to detect pasteboard content change. Keeping only 4 bytes | 65 ClipboardRecentContentImplIOS* implementation) { |
| 97 // is a privacy requirement to introduce collision and allow deniability of | 66 implementation_.reset(implementation); |
| 98 // having copied a given string. | |
| 99 NSData* WeakMD5FromNSString(NSString* string) { | |
| 100 unsigned char hash[CC_MD5_DIGEST_LENGTH]; | |
| 101 const std::string clipboard = base::SysNSStringToUTF8(string); | |
| 102 const char* c_string = clipboard.c_str(); | |
| 103 CC_MD5(c_string, strlen(c_string), hash); | |
| 104 NSData* data = [NSData dataWithBytes:hash length:4]; | |
| 105 return data; | |
| 106 } | 67 } |
| 107 | 68 |
| 108 } // namespace | |
| 109 | |
| 110 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) { | 69 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) { |
| 111 DCHECK(url); | 70 DCHECK(url); |
| 112 UpdateIfNeeded(); | 71 NSURL* url_from_pasteboard = [implementation_ getRecentURLFromClipboard]; |
| 113 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { | 72 GURL converted_url = net::GURLWithNSURL(url_from_pasteboard); |
| 114 return false; | 73 if (converted_url.is_valid()) { |
| 115 } | 74 *url = std::move(converted_url); |
| 116 | |
| 117 GURL url_from_pasteboard = URLFromPasteboard(); | |
| 118 if (url_from_pasteboard.is_valid()) { | |
| 119 *url = url_from_pasteboard; | |
| 120 return true; | 75 return true; |
| 121 } | 76 } |
| 122 return false; | 77 return false; |
| 123 } | 78 } |
| 124 | 79 |
| 80 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {} | |
| 81 | |
| 125 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { | 82 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { |
| 126 return base::TimeDelta::FromSeconds(static_cast<int64_t>( | 83 return base::TimeDelta::FromSeconds( |
| 127 -[last_pasteboard_change_date_ timeIntervalSinceNow])); | 84 static_cast<int64_t>([implementation_ getClipboardContentAge])); |
| 128 } | 85 } |
| 129 | 86 |
| 130 void ClipboardRecentContentIOS::SuppressClipboardContent() { | 87 void ClipboardRecentContentIOS::SuppressClipboardContent() { |
| 131 // User cleared the user data. The pasteboard entry must be removed from the | 88 [implementation_ suppressClipboardContent]; |
| 132 // omnibox list. Force entry expiration by setting copy date to 1970. | |
| 133 last_pasteboard_change_date_.reset( | |
| 134 [[NSDate alloc] initWithTimeIntervalSince1970:0]); | |
| 135 SaveToUserDefaults(); | |
| 136 } | 89 } |
| 137 | |
| 138 void ClipboardRecentContentIOS::UpdateIfNeeded() { | |
| 139 if (!HasPasteboardChanged()) | |
| 140 return; | |
| 141 | |
| 142 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); | |
| 143 | |
| 144 GURL url_from_pasteboard = URLFromPasteboard(); | |
| 145 last_pasteboard_change_date_.reset([[NSDate date] retain]); | |
| 146 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; | |
| 147 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | |
| 148 if (!pasteboard_string) { | |
| 149 pasteboard_string = @""; | |
| 150 } | |
| 151 NSData* MD5 = WeakMD5FromNSString(pasteboard_string); | |
| 152 last_pasteboard_entry_md5_.reset([MD5 retain]); | |
| 153 SaveToUserDefaults(); | |
| 154 } | |
| 155 | |
| 156 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | |
| 157 const std::string& application_scheme, | |
| 158 NSUserDefaults* group_user_defaults) | |
| 159 : application_scheme_(application_scheme), | |
| 160 shared_user_defaults_([group_user_defaults retain]) { | |
| 161 last_pasteboard_change_count_ = NSIntegerMax; | |
| 162 LoadFromUserDefaults(); | |
| 163 | |
| 164 UpdateIfNeeded(); | |
| 165 | |
| 166 // Makes sure |last_pasteboard_change_count_| was properly initialized. | |
| 167 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); | |
| 168 notification_bridge_.reset( | |
| 169 [[ApplicationDidBecomeActiveNotificationListenerBridge alloc] | |
| 170 initWithDelegate:this]); | |
| 171 } | |
| 172 | |
| 173 bool ClipboardRecentContentIOS::HasPasteboardChanged() const { | |
| 174 // If |MD5Changed|, we know for sure there has been at least one pasteboard | |
| 175 // copy since last time it was checked. | |
| 176 // If the pasteboard content is still the same but the device was not | |
| 177 // rebooted, the change count can be checked to see if it changed. | |
| 178 // Note: due to a mismatch between the actual behavior and documentation, and | |
| 179 // lack of consistency on different reboot scenarios, the change count cannot | |
| 180 // be checked after a reboot. | |
| 181 // See radar://21833556 for more information. | |
| 182 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; | |
| 183 bool change_count_changed = change_count != last_pasteboard_change_count_; | |
| 184 | |
| 185 bool not_rebooted = Uptime() > GetClipboardContentAge(); | |
| 186 if (not_rebooted) | |
| 187 return change_count_changed; | |
| 188 | |
| 189 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | |
| 190 if (!pasteboard_string) { | |
| 191 pasteboard_string = @""; | |
| 192 } | |
| 193 NSData* md5 = WeakMD5FromNSString(pasteboard_string); | |
| 194 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; | |
| 195 | |
| 196 return md5_changed; | |
| 197 } | |
| 198 | |
| 199 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { | |
| 200 [notification_bridge_ disconnect]; | |
| 201 } | |
| 202 | |
| 203 GURL ClipboardRecentContentIOS::URLFromPasteboard() { | |
| 204 NSString* clipboard_string = [[UIPasteboard generalPasteboard] string]; | |
| 205 if (!clipboard_string) { | |
| 206 return GURL::EmptyGURL(); | |
| 207 } | |
| 208 const std::string clipboard = base::SysNSStringToUTF8(clipboard_string); | |
| 209 GURL gurl = GURL(clipboard); | |
| 210 if (gurl.is_valid()) { | |
| 211 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { | |
| 212 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { | |
| 213 return gurl; | |
| 214 } | |
| 215 } | |
| 216 if (!application_scheme_.empty() && | |
| 217 gurl.SchemeIs(application_scheme_.c_str())) { | |
| 218 return gurl; | |
| 219 } | |
| 220 } | |
| 221 return GURL::EmptyGURL(); | |
| 222 } | |
| 223 | |
| 224 void ClipboardRecentContentIOS::LoadFromUserDefaults() { | |
| 225 last_pasteboard_change_count_ = | |
| 226 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; | |
| 227 last_pasteboard_change_date_.reset( | |
| 228 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); | |
| 229 last_pasteboard_entry_md5_.reset( | |
| 230 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); | |
| 231 | |
| 232 DCHECK(!last_pasteboard_change_date_ || | |
| 233 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); | |
| 234 } | |
| 235 | |
| 236 void ClipboardRecentContentIOS::SaveToUserDefaults() { | |
| 237 [shared_user_defaults_ setInteger:last_pasteboard_change_count_ | |
| 238 forKey:kPasteboardChangeCountKey]; | |
| 239 [shared_user_defaults_ setObject:last_pasteboard_change_date_ | |
| 240 forKey:kPasteboardChangeDateKey]; | |
| 241 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ | |
| 242 forKey:kPasteboardEntryMD5Key]; | |
| 243 } | |
| 244 | |
| 245 base::TimeDelta ClipboardRecentContentIOS::Uptime() const { | |
| 246 return base::SysInfo::Uptime(); | |
| 247 } | |
| OLD | NEW |