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