blob: 18c43f306791a18d3080891e0dc17ec0c0cdc035 [file] [log] [blame]
// Copyright 2015 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/common/physical_web/physical_web_request.h"
#include "base/ios/weak_nsobject.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_block.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram.h"
#include "base/strings/sys_string_conversions.h"
#include "components/version_info/version_info.h"
#include "google_apis/google_api_keys.h"
#import "ios/chrome/common/physical_web/physical_web_device.h"
#import "ios/chrome/common/physical_web/physical_web_types.h"
#include "ios/web/public/user_agent.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
typedef void (^SessionCompletionProceduralBlock)(NSData* data,
NSURLResponse* response,
NSError* error);
namespace {
NSString* const kUrlsKey = @"urls";
NSString* const kUrlKey = @"url";
NSString* const kResultsKey = @"results";
NSString* const kPageInfoKey = @"pageInfo";
NSString* const kIconKey = @"icon";
NSString* const kTitleKey = @"title";
NSString* const kDescriptionKey = @"description";
NSString* const kScannedUrlKey = @"scannedUrl";
NSString* const kResolvedUrlKey = @"resolvedUrl";
NSString* const kMetadataServiceUrl =
@"https://physicalweb.googleapis.com/v1alpha1/urls:resolve";
NSString* const kKeyQueryItemName = @"key";
NSString* const kHTTPPOSTRequestMethod = @"POST";
std::string GetUserAgent() {
static std::string user_agent;
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
std::string product("CriOS/");
product += version_info::GetVersionNumber();
user_agent = web::BuildUserAgentFromProduct(product);
});
return user_agent;
}
} // namespace
@implementation PhysicalWebRequest {
base::mac::ScopedBlock<physical_web::RequestFinishedBlock> block_;
base::scoped_nsobject<PhysicalWebDevice> device_;
base::scoped_nsobject<NSMutableURLRequest> request_;
base::scoped_nsobject<NSURLSessionDataTask> urlSessionTask_;
base::scoped_nsobject<NSMutableData> data_;
base::scoped_nsobject<NSDate> startDate_;
}
- (instancetype)initWithDevice:(PhysicalWebDevice*)device {
self = [super init];
if (self) {
device_.reset(device);
}
return self;
}
- (instancetype)init {
NOTREACHED();
return nil;
}
- (NSURL*)requestURL {
return [device_ requestURL];
}
- (void)cancel {
[urlSessionTask_ cancel];
block_.reset();
}
- (void)start:(physical_web::RequestFinishedBlock)block {
block_.reset([block copy]);
data_.reset([[NSMutableData alloc] init]);
// Creates the HTTP post request.
base::scoped_nsobject<NSURLComponents> components(
[[NSURLComponents alloc] initWithString:kMetadataServiceUrl]);
NSString* apiKey =
[NSString stringWithUTF8String:google_apis::GetAPIKey().c_str()];
[components
setQueryItems:@[ [NSURLQueryItem queryItemWithName:kKeyQueryItemName
value:apiKey] ]];
NSURL* url = [components URL];
request_.reset([NSMutableURLRequest requestWithURL:url]);
[request_ setHTTPMethod:kHTTPPOSTRequestMethod];
// body of the POST request.
NSDictionary* jsonBody =
@{ kUrlsKey : @[ @{kUrlKey : [[device_ requestURL] absoluteString]} ] };
[request_ setHTTPBody:[NSJSONSerialization dataWithJSONObject:jsonBody
options:0
error:NULL]];
[request_ setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request_ setValue:base::SysUTF8ToNSString(GetUserAgent())
forHTTPHeaderField:@"User-Agent"];
// Set the Accept-Language header from the locale language code. This may
// cause us to fetch metadata for the wrong region in languages such as
// Chinese that vary significantly between regions.
// TODO(mattreynolds): Use the same Accept-Language string as WKWebView.
NSString* acceptLanguage =
[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
[request_ setValue:acceptLanguage forHTTPHeaderField:@"Acccept-Language"];
startDate_.reset([NSDate date]);
// Starts the request.
NSURLSessionConfiguration* sessionConfiguration =
[NSURLSessionConfiguration ephemeralSessionConfiguration];
sessionConfiguration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
NSURLSession* session =
[NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
base::WeakNSObject<PhysicalWebRequest> weakSelf(self);
SessionCompletionProceduralBlock completionHandler =
^(NSData* data, NSURLResponse* response, NSError* error) {
base::scoped_nsobject<PhysicalWebRequest> strongSelf(weakSelf);
if (!strongSelf) {
return;
}
if (error) {
[strongSelf callBlockWithError:error jsonObject:nil];
} else {
[strongSelf.get()->data_ appendData:data];
[strongSelf sessionDidFinishLoading];
}
};
urlSessionTask_.reset([session dataTaskWithRequest:request_
completionHandler:completionHandler]);
[urlSessionTask_ resume];
}
- (void)sessionDidFinishLoading {
NSError* error = nil;
NSDictionary* jsonObject =
[NSJSONSerialization JSONObjectWithData:data_ options:0 error:&error];
if (error != nil) {
[self callBlockWithError:error jsonObject:nil];
return;
}
if (![self isValidJSON:jsonObject]) {
[self callBlockWithError:
[NSError errorWithDomain:physical_web::kPhysicalWebErrorDomain
code:physical_web::ERROR_INVALID_JSON
userInfo:nil]
jsonObject:jsonObject];
return;
}
[self callBlockWithError:error jsonObject:jsonObject];
}
// Checks whether the JSON object has the correct format.
// The format should be similar to the following:
// {
// "results":[
// {
// "pageInfo":{
// "title":"Google"
// "description":"Search the world's information"
// "icon":"https://www.google.com/favicon.ico"
// }
// "scannedUrl":"https://www.google.com"
// "resolvedUrl":"https://www.google.com"
// }
// ]
// }
- (BOOL)isValidJSON:(id)jsonObject {
NSDictionary* dict = base::mac::ObjCCast<NSDictionary>(jsonObject);
if (!dict) {
return NO;
}
NSArray* list = base::mac::ObjCCast<NSArray>(dict[kResultsKey]);
if (!list) {
return NO;
}
if ([list count] != 1) {
return NO;
}
NSDictionary* item = base::mac::ObjCCast<NSDictionary>(list[0]);
if (!item) {
return NO;
}
return YES;
}
// Calls the block passed as parameter of -start: when the request is finished.
- (void)callBlockWithError:(NSError*)error
jsonObject:(NSDictionary*)jsonObject {
if (error) {
if (block_.get()) {
block_.get()(nil, error);
}
} else {
NSTimeInterval roundTripTime =
[[NSDate date] timeIntervalSinceDate:startDate_];
int roundTripTimeMS = 1000 * roundTripTime;
UMA_HISTOGRAM_COUNTS("PhysicalWeb.RoundTripTimeMilliseconds",
roundTripTimeMS);
NSArray* list = jsonObject[kResultsKey];
NSDictionary* item = list[0];
NSDictionary* pageInfo =
base::mac::ObjCCast<NSDictionary>(item[kPageInfoKey]);
NSString* scannedUrlString =
base::mac::ObjCCast<NSString>(item[kScannedUrlKey]);
NSString* resolvedUrlString =
base::mac::ObjCCast<NSString>(item[kResolvedUrlKey]);
// Verify required fields pageInfo, scannedUrl, and resolvedUrl are present.
if (pageInfo == nil || scannedUrlString == nil ||
resolvedUrlString == nil) {
error = [NSError errorWithDomain:physical_web::kPhysicalWebErrorDomain
code:physical_web::ERROR_INVALID_JSON
userInfo:nil];
if (block_.get()) {
block_.get()(nil, error);
}
return;
}
// Read optional fields.
NSString* iconString = base::mac::ObjCCast<NSString>(pageInfo[kIconKey]);
NSString* description =
base::mac::ObjCCast<NSString>(pageInfo[kDescriptionKey]);
NSString* title = base::mac::ObjCCast<NSString>(pageInfo[kTitleKey]);
NSURL* resolvedUrl =
resolvedUrlString ? [NSURL URLWithString:resolvedUrlString] : nil;
NSURL* icon = iconString ? [NSURL URLWithString:iconString] : nil;
base::scoped_nsobject<PhysicalWebDevice> device([[PhysicalWebDevice alloc]
initWithURL:resolvedUrl
requestURL:[device_ requestURL]
icon:icon
title:title
description:description
transmitPower:[device_ transmitPower]
rssi:[device_ rssi]
rank:physical_web::kMaxRank
scanTimestamp:[device_ scanTimestamp]]);
if (block_.get() != nil) {
block_.get()(device, nil);
}
}
}
@end