// Copyright 2014 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.

#import "ios/chrome/browser/crash_report/crash_report_background_uploader.h"

#import <UIKit/UIKit.h>

#include "base/logging.h"
#include "base/mac/scoped_block.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram.h"
#include "base/metrics/user_metrics_action.h"
#include "base/time/time.h"
#import "breakpad/src/client/ios/BreakpadController.h"
#include "ios/chrome/browser/experimental_flags.h"
#include "ios/web/public/user_metrics.h"

using base::UserMetricsAction;

namespace {

NSString* const kBackgroundReportUploader =
    @"com.google.chrome.breakpad.backgroundupload";
const char* const kUMAMobileCrashBackgroundUploadDelay =
    "CrashReport.CrashBackgroundUploadDelay";
const char* const kUMAMobilePendingReportsOnBackgroundWakeUp =
    "CrashReport.PendingReportsOnBackgroundWakeUp";
NSString* const kUploadedInBackground = @"uploaded_in_background";
NSString* const kReportsUploadedInBackground = @"ReportsUploadedInBackground";

NSString* CreateSessionIdentifierFromTask(NSURLSessionTask* task) {
  return [NSString stringWithFormat:@"%@.%ld", kBackgroundReportUploader,
                                    (unsigned long)[task taskIdentifier]];
}

}  // namespace

@interface UrlSessionDelegate : NSObject<NSURLSessionDelegate,
                                         NSURLSessionTaskDelegate,
                                         NSURLSessionDataDelegate>
+ (instancetype)sharedInstance;

// Sets the completion handler for the URL session current tasks. The
// |completionHandler| cannot be nil.
- (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler;

@end

@implementation UrlSessionDelegate {
  // The completion handler to call when all tasks are completed.
  base::mac::ScopedBlock<ProceduralBlock> _sessionCompletionHandler;
  // The number of tasks in progress for the session.
  int _tasks;
  // Flag to indicate that URLSessionDidFinishEventsForBackgroundURLSession
  // has been called, so that no new task will be launched for this session.
  // It is safe to call completion handler when the pending tasks are completed.
  BOOL _didFinishEventsCalled;
}

+ (instancetype)sharedInstance {
  static UrlSessionDelegate* instance = [[UrlSessionDelegate alloc] init];
  return instance;
}

- (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler {
  DCHECK(completionHandler);
  _sessionCompletionHandler.reset(completionHandler,
                                  base::scoped_policy::RETAIN);
  _didFinishEventsCalled = NO;
}

- (void)URLSession:(NSURLSession*)session
                   task:(NSURLSessionTask*)dataTask
    didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
      completionHandler:
          (void (^)(NSURLSessionAuthChallengeDisposition disposition,
                    NSURLCredential* credential))completionHandler {
  if (![challenge.protectionSpace.authenticationMethod
          isEqualToString:NSURLAuthenticationMethodServerTrust]) {
    completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
    return;
  }
  NSString* identifier = CreateSessionIdentifierFromTask(dataTask);

  NSDictionary* configuration =
      [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier];
  NSString* host =
      [[NSURL URLWithString:[configuration objectForKey:@BREAKPAD_URL]] host];
  if ([challenge.protectionSpace.host isEqualToString:host]) {
    NSURLCredential* credential = [NSURLCredential
        credentialForTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    return;
  }
  completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:
        (NSURLSession*)session {
  _didFinishEventsCalled = YES;
  [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self callCompletionHandler];
  }];
}

- (void)taskFinished {
  DCHECK_GT(_tasks, 0);
  _tasks--;
  [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self callCompletionHandler];
  }];
}

- (void)callCompletionHandler {
  if (_tasks > 0 || !_didFinishEventsCalled)
    return;
  if (_sessionCompletionHandler) {
    void (^completionHandler)() = _sessionCompletionHandler.get();
    completionHandler();
    _sessionCompletionHandler.reset();
  }
}

- (void)URLSession:(NSURLSession*)session
              dataTask:(NSURLSessionDataTask*)dataTask
    didReceiveResponse:(NSURLResponse*)response
     completionHandler:
         (void (^)(NSURLSessionResponseDisposition disposition))handler {
  handler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession*)session
          dataTask:(NSURLSessionDataTask*)dataTask
    didReceiveData:(NSData*)data {
  NSString* identifier = CreateSessionIdentifierFromTask(dataTask);

  NSDictionary* configuration =
      [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier];
  [[NSUserDefaults standardUserDefaults] removeObjectForKey:identifier];
  _tasks++;

  if (experimental_flags::IsAlertOnBackgroundUploadEnabled()) {
    base::scoped_nsobject<UILocalNotification> localNotification(
        [[UILocalNotification alloc] init]);
    localNotification.get().fireDate = [NSDate date];
    base::scoped_nsobject<NSString> reportId(
        [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    localNotification.get().alertBody = [NSString
        stringWithFormat:@"Crash report uploaded: %@", reportId.get()];
    [[UIApplication sharedApplication]
        scheduleLocalNotification:localNotification];
  }

  [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
    BreakpadHandleNetworkResponse(ref, configuration, data, nil);
    dispatch_async(dispatch_get_main_queue(), ^{
      [self taskFinished];
    });
  }];
}

@end

@implementation CrashReportBackgroundUploader

@synthesize hasPendingCrashReportsToUploadAtStartup;

+ (instancetype)sharedInstance {
  static CrashReportBackgroundUploader* instance =
      [[CrashReportBackgroundUploader alloc] init];
  return instance;
}

+ (NSURLSession*)BreakpadBackgroundURLSessionWithCompletionHandler:
        (ProceduralBlock)completionHandler {
  static NSURLSession* session = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration
        backgroundSessionConfigurationWithIdentifier:kBackgroundReportUploader];
    session = [NSURLSession
        sessionWithConfiguration:sessionConfig
                        delegate:[UrlSessionDelegate sharedInstance]
                   delegateQueue:[NSOperationQueue mainQueue]];
  });
  DCHECK(session);
  if (completionHandler) {
    [[UrlSessionDelegate sharedInstance]
        setSessionCompletionHandler:completionHandler];
  }
  return session;
}

+ (BOOL)sendNextReport:(NSDictionary*)nextReport
       withBreakpadRef:(BreakpadRef)ref {
  NSString* uploadURL =
      [NSString stringWithString:[nextReport valueForKey:@BREAKPAD_URL]];
  NSString* tmpDir = NSTemporaryDirectory();
  NSString* tmpFile = [tmpDir
      stringByAppendingPathComponent:
          [NSString
              stringWithFormat:@"%.0f.%@",
                               [NSDate timeIntervalSinceReferenceDate] * 1000.0,
                               @"txt"]];
  NSURL* fileURL = [NSURL fileURLWithPath:tmpFile];
  [nextReport setValue:[fileURL absoluteString] forKey:@BREAKPAD_URL];

#ifndef NDEBUG
  NSString* BreakpadMinidumpLocation = [NSHomeDirectory()
      stringByAppendingPathComponent:@"Library/Caches/Breakpad"];
  [nextReport setValue:BreakpadMinidumpLocation
                forKey:@kReporterMinidumpDirectoryKey];
  [nextReport setValue:BreakpadMinidumpLocation
                forKey:@BREAKPAD_DUMP_DIRECTORY];
#endif

  [[BreakpadController sharedInstance]
      threadUnsafeSendReportWithConfiguration:nextReport
                              withBreakpadRef:ref];

  NSFileManager* fileManager = [NSFileManager defaultManager];
  if (![fileManager fileExistsAtPath:tmpFile]) {
    return NO;
  }

  NSError* error;
  NSString* fileString =
      [NSString stringWithContentsOfFile:tmpFile
                                encoding:NSISOLatin1StringEncoding
                                   error:&error];

  // The HTTP content is a MIME multipart. The delimiter of the mime body must
  // be added to the HTTP headers.
  // A mime body is of the form
  // --{delimiter}
  // content 1
  // --{delimiter}
  // content 2
  // --{delimiter}--
  // The delimiter can be read on the first line of the file.
  NSString* delimiter =
      [[fileString componentsSeparatedByCharactersInSet:
                       [NSCharacterSet newlineCharacterSet]] firstObject];
  if (![delimiter hasPrefix:@"--"]) {
    [fileManager removeItemAtPath:tmpFile error:&error];
    return NO;
  }
  delimiter = [[delimiter
      stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]
      substringFromIndex:2];

  NSMutableURLRequest* request =
      [NSMutableURLRequest requestWithURL:[NSURL URLWithString:uploadURL]];
  [request setHTTPMethod:@"POST"];
  [request setValue:[NSString
                        stringWithFormat:@"multipart/form-data; boundary=%@",
                                         delimiter]
      forHTTPHeaderField:@"Content-type"];
  [request setHTTPBody:[NSData dataWithContentsOfFile:tmpFile]];

  NSURLSession* session = [CrashReportBackgroundUploader
      BreakpadBackgroundURLSessionWithCompletionHandler:nil];
  NSURLSessionDataTask* dataTask =
      [session uploadTaskWithRequest:request fromFile:fileURL];

  NSString* identifier = CreateSessionIdentifierFromTask(dataTask);
  [[NSUserDefaults standardUserDefaults] setObject:nextReport
                                            forKey:identifier];

  [dataTask resume];
  return YES;
}

+ (void)performFetchWithCompletionHandler:
        (BackgroundFetchCompletionBlock)completionHandler {
  [[BreakpadController sharedInstance] stop];
  [[BreakpadController sharedInstance] setParametersToAddAtUploadTime:@{
    kUploadedInBackground : @"yes"
  }];
  [[BreakpadController sharedInstance] start:YES];
  [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
    // Note that this processing will be done before |sendNextCrashReport|
    // starts uploading the crashes. The ordering is ensured here because both
    // the crash report processing and the upload enabling are handled by
    // posting blocks to a single |dispath_queue_t| in BreakpadController.
    [[BreakpadController sharedInstance] setUploadingEnabled:YES];
    [[BreakpadController sharedInstance]
        getNextReportConfigurationOrSendDelay:^(NSDictionary* nextReport,
                                                int delay) {
          BOOL reportToSend = NO;
          BOOL uploaded = NO;
          UMA_HISTOGRAM_COUNTS_100(kUMAMobilePendingReportsOnBackgroundWakeUp,
                                   BreakpadGetCrashReportCount(ref));
          if (delay == 0 && nextReport) {
            reportToSend = YES;
            NSNumber* crashTimeNum =
                [nextReport valueForKey:@BREAKPAD_PROCESS_CRASH_TIME];
            base::Time crashTime =
                base::Time::FromTimeT([crashTimeNum intValue]);
            base::Time now = base::Time::Now();
            UMA_HISTOGRAM_LONG_TIMES_100(kUMAMobileCrashBackgroundUploadDelay,
                                         now - crashTime);
            uploaded = [self sendNextReport:nextReport withBreakpadRef:ref];
          }
          int pendingReports = BreakpadGetCrashReportCount(ref);
          [[BreakpadController sharedInstance] setUploadingEnabled:NO];
          dispatch_async(dispatch_get_main_queue(), ^{
            if (reportToSend) {
              if (uploaded) {
                NSUserDefaults* defaults =
                    [NSUserDefaults standardUserDefaults];
                NSInteger uploadedCrashes =
                    [defaults integerForKey:kReportsUploadedInBackground];
                [defaults setInteger:(uploadedCrashes + 1)
                              forKey:kReportsUploadedInBackground];
                web::RecordAction(
                    UserMetricsAction("BackgroundUploadReportSucceeded"));

              } else {
                web::RecordAction(
                    UserMetricsAction("BackgroundUploadReportAborted"));
              }
            }
            if (uploaded && pendingReports) {
              completionHandler(UIBackgroundFetchResultNewData);
            } else if (pendingReports) {
              completionHandler(UIBackgroundFetchResultFailed);
            } else {
              [[UIApplication sharedApplication]
                  setMinimumBackgroundFetchInterval:
                      UIApplicationBackgroundFetchIntervalNever];
              completionHandler(UIBackgroundFetchResultNoData);
            }
          });
        }];
  }];
}

+ (BOOL)canHandleBackgroundURLSession:(NSString*)identifier {
  return [identifier isEqualToString:kBackgroundReportUploader];
}

+ (void)handleEventsForBackgroundURLSession:(NSString*)identifier
                          completionHandler:(ProceduralBlock)completionHandler {
  [CrashReportBackgroundUploader
      BreakpadBackgroundURLSessionWithCompletionHandler:completionHandler];
}

+ (BOOL)hasUploadedCrashReportsInBackground {
  NSInteger uploadedCrashReportsInBackgroundCount =
      [[NSUserDefaults standardUserDefaults]
          integerForKey:kReportsUploadedInBackground];
  return uploadedCrashReportsInBackgroundCount > 0;
}

+ (void)resetReportsUploadedInBackgroundCount {
  [[NSUserDefaults standardUserDefaults]
      removeObjectForKey:kReportsUploadedInBackground];
}

@end