1// Copyright 2016 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 5#import "ios/chrome/browser/share_extension/share_extension_item_receiver.h" 6 7#import <UIKit/UIKit.h> 8 9#include "base/bind.h" 10#include "base/ios/block_types.h" 11#include "base/mac/foundation_util.h" 12#include "base/metrics/histogram_macros.h" 13#include "base/metrics/user_metrics_action.h" 14#include "base/sequenced_task_runner.h" 15#include "base/strings/sys_string_conversions.h" 16#include "base/strings/utf_string_conversions.h" 17#include "base/task/post_task.h" 18#include "base/task/thread_pool.h" 19#include "base/threading/scoped_blocking_call.h" 20#include "components/bookmarks/browser/bookmark_model.h" 21#include "components/reading_list/core/reading_list_model.h" 22#include "components/reading_list/core/reading_list_model_observer.h" 23#include "ios/chrome/browser/system_flags.h" 24#include "ios/chrome/common/app_group/app_group_constants.h" 25#include "ios/web/public/thread/web_task_traits.h" 26#include "ios/web/public/thread/web_thread.h" 27#import "net/base/mac/url_conversions.h" 28#include "url/gurl.h" 29 30#if !defined(__has_feature) || !__has_feature(objc_arc) 31#error "This file requires ARC support." 32#endif 33 34namespace { 35// Enum used to send metrics on item reception. 36// If you change this enum, update histograms.xml. 37enum ShareExtensionItemReceived { 38 INVALID_ENTRY = 0, 39 CANCELLED_ENTRY, 40 READINGLIST_ENTRY, 41 BOOKMARK_ENTRY, 42 OPEN_IN_CHROME_ENTRY, 43 SHARE_EXTENSION_ITEM_RECEIVED_COUNT 44}; 45 46// Enum used to send metrics on item reception. 47// If you change this enum, update histograms.xml. 48enum ShareExtensionSource { 49 UNKNOWN_SOURCE = 0, 50 SHARE_EXTENSION, 51 SHARE_EXTENSION_SOURCE_COUNT 52}; 53 54ShareExtensionSource SourceIDFromSource(NSString* source) { 55 if ([source isEqualToString:app_group::kShareItemSourceShareExtension]) { 56 return SHARE_EXTENSION; 57 } 58 return UNKNOWN_SOURCE; 59} 60 61void LogHistogramReceivedItem(ShareExtensionItemReceived type) { 62 UMA_HISTOGRAM_ENUMERATION("IOS.ShareExtension.ReceivedEntry", type, 63 SHARE_EXTENSION_ITEM_RECEIVED_COUNT); 64} 65 66} // namespace 67 68@interface ShareExtensionItemReceiver ()<NSFilePresenter> { 69 BOOL _isObservingReadingListFolder; 70 BOOL _readingListFolderCreated; 71 ReadingListModel* _readingListModel; 72 bookmarks::BookmarkModel* _bookmarkModel; 73 scoped_refptr<base::SequencedTaskRunner> _taskRunner; 74} 75 76// Checks if the reading list folder is already created and if not, create it. 77- (void)createReadingListFolder; 78 79// Invoked on UI thread once the reading list folder has been created. 80- (void)readingListFolderCreated; 81 82// Processes the data sent by the share extension. Data should be a NSDictionary 83// serialized by +|NSKeyedArchiver archivedDataWithRootObject:|. 84// |completion| is called if |data| has been fully processed. 85- (BOOL)receivedData:(NSData*)data withCompletion:(ProceduralBlock)completion; 86 87// Reads the file pointed by |url| and calls |receivedData:| on the content. 88// If the file is processed, delete it. 89// |completion| is only called if the file handling is completed without error. 90- (void)handleFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion; 91 92// Deletes the file pointed by |url| then call |completion|. 93- (void)deleteFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion; 94 95// Called on UIApplicationDidBecomeActiveNotification notification. 96- (void)applicationDidBecomeActive; 97 98// Processes files that are already in the folder and starts observing the 99// app_group::ShareExtensionItemsFolder() folder for new files. 100- (void)processExistingFiles; 101 102// Invoked with the list of pre-existing files in the folder to process them. 103- (void)entriesReceived:(NSArray<NSURL*>*)files; 104 105// Called on UIApplicationWillResignActiveNotification. Stops observing the 106// app_group::ShareExtensionItemsFolder() folder for new files. 107- (void)applicationWillResignActive; 108 109// Called whenever a file is modified in app_group::ShareExtensionItemsFolder(). 110- (void)presentedSubitemDidChangeAtURL:(NSURL*)url; 111 112@end 113 114@implementation ShareExtensionItemReceiver 115 116#pragma mark - NSObject lifetime 117 118- (void)dealloc { 119 DCHECK(!_taskRunner) << "-shutdown must be called before -dealloc"; 120} 121 122#pragma mark - Public API 123 124- (instancetype)initWithBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel 125 readingListModel:(ReadingListModel*)readingListModel { 126 DCHECK(bookmarkModel); 127 DCHECK(readingListModel); 128 129 self = [super init]; 130 if (![self presentedItemURL]) 131 return nil; 132 133 if (self) { 134 _readingListModel = readingListModel; 135 _bookmarkModel = bookmarkModel; 136 _taskRunner = base::ThreadPool::CreateSequencedTaskRunner( 137 {base::MayBlock(), base::TaskPriority::BEST_EFFORT}); 138 139 [[NSNotificationCenter defaultCenter] 140 addObserver:self 141 selector:@selector(applicationDidBecomeActive) 142 name:UIApplicationDidBecomeActiveNotification 143 object:nil]; 144 [[NSNotificationCenter defaultCenter] 145 addObserver:self 146 selector:@selector(applicationWillResignActive) 147 name:UIApplicationWillResignActiveNotification 148 object:nil]; 149 150 __weak ShareExtensionItemReceiver* weakSelf = self; 151 _taskRunner->PostTask(FROM_HERE, base::BindOnce(^{ 152 [weakSelf createReadingListFolder]; 153 })); 154 } 155 156 return self; 157} 158 159- (void)shutdown { 160 [[NSNotificationCenter defaultCenter] removeObserver:self]; 161 if (_isObservingReadingListFolder) { 162 [NSFileCoordinator removeFilePresenter:self]; 163 } 164 _readingListModel = nil; 165 _bookmarkModel = nil; 166 _taskRunner = nullptr; 167} 168 169#pragma mark - Private API 170 171- (void)createReadingListFolder { 172 { 173 base::ScopedBlockingCall scoped_blocking_call( 174 FROM_HERE, base::BlockingType::WILL_BLOCK); 175 NSFileManager* manager = [NSFileManager defaultManager]; 176 if (![manager fileExistsAtPath:[[self presentedItemURL] path]]) { 177 [manager createDirectoryAtPath:[[self presentedItemURL] path] 178 withIntermediateDirectories:NO 179 attributes:nil 180 error:nil]; 181 } 182 } 183 184 __weak ShareExtensionItemReceiver* weakSelf = self; 185 base::PostTask(FROM_HERE, {web::WebThread::UI}, base::BindOnce(^{ 186 [weakSelf readingListFolderCreated]; 187 })); 188} 189 190- (void)readingListFolderCreated { 191 UIApplication* application = [UIApplication sharedApplication]; 192 if ([application applicationState] == UIApplicationStateActive) { 193 _readingListFolderCreated = YES; 194 [self applicationDidBecomeActive]; 195 } 196} 197 198- (BOOL)receivedData:(NSData*)data withCompletion:(ProceduralBlock)completion { 199 NSError* error = nil; 200 NSKeyedUnarchiver* unarchiver = 201 [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error]; 202 if (!unarchiver || error) { 203 DLOG(WARNING) << "Error creating share extension item unarchiver: " 204 << base::SysNSStringToUTF8([error description]); 205 return NO; 206 } 207 208 unarchiver.requiresSecureCoding = NO; 209 210 id entryID = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey]; 211 NSDictionary* entry = base::mac::ObjCCast<NSDictionary>(entryID); 212 if (!entry) { 213 if (completion) { 214 completion(); 215 } 216 return NO; 217 } 218 219 NSNumber* cancelled = base::mac::ObjCCast<NSNumber>( 220 [entry objectForKey:app_group::kShareItemCancel]); 221 if (!cancelled) { 222 if (completion) { 223 completion(); 224 } 225 return NO; 226 } 227 if ([cancelled boolValue]) { 228 LogHistogramReceivedItem(CANCELLED_ENTRY); 229 if (completion) { 230 completion(); 231 } 232 return YES; 233 } 234 235 GURL entryURL = 236 net::GURLWithNSURL([entry objectForKey:app_group::kShareItemURL]); 237 std::string entryTitle = 238 base::SysNSStringToUTF8([entry objectForKey:app_group::kShareItemTitle]); 239 NSDate* entryDate = base::mac::ObjCCast<NSDate>( 240 [entry objectForKey:app_group::kShareItemDate]); 241 NSNumber* entryType = base::mac::ObjCCast<NSNumber>( 242 [entry objectForKey:app_group::kShareItemType]); 243 NSString* entrySource = base::mac::ObjCCast<NSString>( 244 [entry objectForKey:app_group::kShareItemSource]); 245 246 if (!entryURL.is_valid() || !entrySource || !entryDate || !entryType || 247 !entryURL.SchemeIsHTTPOrHTTPS()) { 248 if (completion) { 249 completion(); 250 } 251 return NO; 252 } 253 254 UMA_HISTOGRAM_TIMES("IOS.ShareExtension.ReceivedEntryDelay", 255 base::TimeDelta::FromSecondsD( 256 [[NSDate date] timeIntervalSinceDate:entryDate])); 257 258 UMA_HISTOGRAM_ENUMERATION("IOS.ShareExtension.Source", 259 SourceIDFromSource(entrySource), 260 SHARE_EXTENSION_SOURCE_COUNT); 261 262 // Entry is valid. Add it to the reading list model. 263 ProceduralBlock processEntryBlock = ^{ 264 if (!_readingListModel || !_bookmarkModel) { 265 // Models may have been deleted after the file 266 // processing started. 267 return; 268 } 269 app_group::ShareExtensionItemType type = 270 static_cast<app_group::ShareExtensionItemType>( 271 [entryType integerValue]); 272 switch (type) { 273 case app_group::READING_LIST_ITEM: { 274 LogHistogramReceivedItem(READINGLIST_ENTRY); 275 _readingListModel->AddEntry(entryURL, entryTitle, 276 reading_list::ADDED_VIA_EXTENSION); 277 break; 278 } 279 case app_group::BOOKMARK_ITEM: { 280 LogHistogramReceivedItem(BOOKMARK_ENTRY); 281 _bookmarkModel->AddURL(_bookmarkModel->mobile_node(), 0, 282 base::UTF8ToUTF16(entryTitle), entryURL); 283 break; 284 } 285 case app_group::OPEN_IN_CHROME_ITEM: { 286 LogHistogramReceivedItem(OPEN_IN_CHROME_ENTRY); 287 // Open URL command is sent directly by the extension. No processing is 288 // needed here. 289 break; 290 } 291 } 292 293 if (completion && _taskRunner) { 294 _taskRunner->PostTask(FROM_HERE, base::BindOnce(^{ 295 completion(); 296 })); 297 } 298 }; 299 base::PostTask(FROM_HERE, {web::WebThread::UI}, 300 base::BindOnce(processEntryBlock)); 301 return YES; 302} 303 304- (void)handleFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion { 305 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, 306 base::BlockingType::WILL_BLOCK); 307 if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { 308 // The handler is called on file modification, including deletion. Check 309 // that the file exists before continuing. 310 return; 311 } 312 __weak ShareExtensionItemReceiver* weakSelf = self; 313 ProceduralBlock successCompletion = ^{ 314 [weakSelf deleteFileAtURL:url withCompletion:completion]; 315 }; 316 void (^readingAccessor)(NSURL*) = ^(NSURL* newURL) { 317 base::ScopedBlockingCall scoped_blocking_call( 318 FROM_HERE, base::BlockingType::WILL_BLOCK); 319 NSFileManager* manager = [NSFileManager defaultManager]; 320 NSData* data = [manager contentsAtPath:[newURL path]]; 321 if (![weakSelf receivedData:data withCompletion:successCompletion]) { 322 LogHistogramReceivedItem(INVALID_ENTRY); 323 } 324 }; 325 NSError* error = nil; 326 NSFileCoordinator* readingCoordinator = 327 [[NSFileCoordinator alloc] initWithFilePresenter:self]; 328 [readingCoordinator 329 coordinateReadingItemAtURL:url 330 options:NSFileCoordinatorReadingWithoutChanges 331 error:&error 332 byAccessor:readingAccessor]; 333} 334 335- (void)deleteFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion { 336 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, 337 base::BlockingType::WILL_BLOCK); 338 void (^deletingAccessor)(NSURL*) = ^(NSURL* newURL) { 339 base::ScopedBlockingCall scoped_blocking_call( 340 FROM_HERE, base::BlockingType::MAY_BLOCK); 341 NSFileManager* manager = [NSFileManager defaultManager]; 342 [manager removeItemAtURL:newURL error:nil]; 343 }; 344 NSError* error = nil; 345 NSFileCoordinator* deletingCoordinator = 346 [[NSFileCoordinator alloc] initWithFilePresenter:self]; 347 [deletingCoordinator 348 coordinateWritingItemAtURL:url 349 options:NSFileCoordinatorWritingForDeleting 350 error:&error 351 byAccessor:deletingAccessor]; 352 if (completion) { 353 completion(); 354 } 355} 356 357- (void)applicationDidBecomeActive { 358 if (!_readingListFolderCreated || _isObservingReadingListFolder) { 359 return; 360 } 361 _isObservingReadingListFolder = YES; 362 363 // Start observing for new files. 364 [NSFileCoordinator addFilePresenter:self]; 365 366 // There may already be files. Process them. 367 if (_taskRunner) { 368 __weak ShareExtensionItemReceiver* weakSelf = self; 369 _taskRunner->PostTask(FROM_HERE, base::BindOnce(^{ 370 [weakSelf processExistingFiles]; 371 })); 372 } 373} 374 375- (void)processExistingFiles { 376 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, 377 base::BlockingType::WILL_BLOCK); 378 NSMutableArray<NSURL*>* files = [NSMutableArray array]; 379 NSFileManager* manager = [NSFileManager defaultManager]; 380 NSArray<NSURL*>* oldFiles = [manager 381 contentsOfDirectoryAtURL:app_group::LegacyShareExtensionItemsFolder() 382 includingPropertiesForKeys:nil 383 options:NSDirectoryEnumerationSkipsHiddenFiles 384 error:nil]; 385 [files addObjectsFromArray:oldFiles]; 386 387 NSArray<NSURL*>* newFiles = 388 [manager contentsOfDirectoryAtURL:[self presentedItemURL] 389 includingPropertiesForKeys:nil 390 options:NSDirectoryEnumerationSkipsHiddenFiles 391 error:nil]; 392 [files addObjectsFromArray:newFiles]; 393 394 if ([files count]) { 395 __weak ShareExtensionItemReceiver* weakSelf = self; 396 base::PostTask(FROM_HERE, {web::WebThread::UI}, base::BindOnce(^{ 397 [weakSelf entriesReceived:files]; 398 })); 399 } 400} 401 402- (void)entriesReceived:(NSArray<NSURL*>*)files { 403 UMA_HISTOGRAM_COUNTS_100("IOS.ShareExtension.ReceivedEntriesCount", 404 [files count]); 405 if (!_taskRunner) 406 return; 407 408 __weak ShareExtensionItemReceiver* weakSelf = self; 409 for (NSURL* fileURL : files) { 410 __block std::unique_ptr<ReadingListModel::ScopedReadingListBatchUpdate> 411 batchToken(_readingListModel->BeginBatchUpdates()); 412 _taskRunner->PostTask(FROM_HERE, base::BindOnce(^{ 413 [weakSelf handleFileAtURL:fileURL 414 withCompletion:^{ 415 base::PostTask(FROM_HERE, 416 {web::WebThread::UI}, 417 base::BindOnce(^{ 418 batchToken.reset(); 419 })); 420 }]; 421 })); 422 } 423} 424 425- (void)applicationWillResignActive { 426 if (!_isObservingReadingListFolder) { 427 return; 428 } 429 _isObservingReadingListFolder = NO; 430 [NSFileCoordinator removeFilePresenter:self]; 431} 432 433#pragma mark - NSFilePresenter methods 434 435- (void)presentedSubitemDidChangeAtURL:(NSURL*)url { 436 if (_taskRunner) { 437 __weak ShareExtensionItemReceiver* weakSelf = self; 438 _taskRunner->PostTask(FROM_HERE, base::BindOnce(^{ 439 [weakSelf handleFileAtURL:url withCompletion:nil]; 440 })); 441 } 442} 443 444- (NSOperationQueue*)presentedItemOperationQueue { 445 return [NSOperationQueue mainQueue]; 446} 447 448- (NSURL*)presentedItemURL { 449 return app_group::ExternalCommandsItemsFolder(); 450} 451 452@end 453