You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

545 lines
22 KiB

//
// FLEXNetworkTransactionDetailTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXNetworkTransactionDetailTableViewController.h"
#import "FLEXNetworkCurlLogger.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXUtility.h"
#import "FLEXManager+Private.h"
#import "FLEXTableView.h"
typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
@interface FLEXNetworkDetailRow : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *detailText;
@property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
@end
@implementation FLEXNetworkDetailRow
@end
@interface FLEXNetworkDetailSection : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSArray<FLEXNetworkDetailRow *> *rows;
@end
@implementation FLEXNetworkDetailSection
@end
@interface FLEXNetworkTransactionDetailTableViewController ()
@property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
@end
@implementation FLEXNetworkTransactionDetailTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style
{
// Force grouped style
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy curl" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonPressed:)];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineCell];
}
- (void)setTransaction:(FLEXNetworkTransaction *)transaction
{
if (![_transaction isEqual:transaction]) {
_transaction = transaction;
self.title = [transaction.request.URL lastPathComponent];
[self rebuildTableSections];
}
}
- (void)setSections:(NSArray<FLEXNetworkDetailSection *> *)sections
{
if (![_sections isEqual:sections]) {
_sections = [sections copy];
[self.tableView reloadData];
}
}
- (void)rebuildTableSections
{
NSMutableArray<FLEXNetworkDetailSection *> *sections = [NSMutableArray array];
FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
if (generalSection.rows.count > 0) {
[sections addObject:generalSection];
}
FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
if (requestHeadersSection.rows.count > 0) {
[sections addObject:requestHeadersSection];
}
FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
if (queryParametersSection.rows.count > 0) {
[sections addObject:queryParametersSection];
}
FLEXNetworkDetailSection *postBodySection = [[self class] postBodySectionForTransaction:self.transaction];
if (postBodySection.rows.count > 0) {
[sections addObject:postBodySection];
}
FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
if (responseHeadersSection.rows.count > 0) {
[sections addObject:responseHeadersSection];
}
self.sections = sections;
}
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
{
FLEXNetworkTransaction *transaction = [[notification userInfo] objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
if (transaction == self.transaction) {
[self rebuildTableSections];
}
}
- (void)copyButtonPressed:(id)sender
{
[UIPasteboard.generalPasteboard setString:[FLEXNetworkCurlLogger curlCommandString:_transaction.request]];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
FLEXNetworkDetailSection *sectionModel = self.sections[section];
return sectionModel.rows.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
FLEXNetworkDetailSection *sectionModel = self.sections[section];
return sectionModel.title;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineCell forIndexPath:indexPath];
FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
UIViewController *viewController = nil;
if (rowModel.selectionFuture) {
viewController = rowModel.selectionFuture();
}
if ([viewController isKindOfClass:UIAlertController.class]) {
[self presentViewController:viewController animated:YES completion:nil];
} else if (viewController) {
[self.navigationController pushViewController:viewController animated:YES];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
BOOL showsAccessory = row.selectionFuture != nil;
return [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.bounds.size.width style:UITableViewStyleGrouped showsAccessory:showsAccessory];
}
- (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailSection *sectionModel = self.sections[indexPath.section];
return sectionModel.rows[indexPath.row];
}
#pragma mark - Cell Copying
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(copy:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = row.detailText;
}
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0)
{
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction
actionWithTitle:@"Copy"
image:nil
identifier:nil
handler:^(__kindof UIAction *action) {
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = row.detailText;
}
];
return [UIMenu
menuWithTitle:@"" image:nil identifier:nil
options:UIMenuOptionsDisplayInline
children:@[copy]
];
}
];
}
#endif
#pragma mark - View Configuration
+ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row
{
NSDictionary<NSString *, id> *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
NSDictionary<NSString *, id> *detailAttributes = @{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont],
NSForegroundColorAttributeName : [FLEXColor primaryTextColor] };
NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
NSString *detailText = row.detailText ?: @"";
NSMutableAttributedString *attributedText = [NSMutableAttributedString new];
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
return attributedText;
}
#pragma mark - Table Data Generation
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray array];
FLEXNetworkDetailRow *requestURLRow = [FLEXNetworkDetailRow new];
requestURLRow.title = @"Request URL";
NSURL *url = transaction.request.URL;
requestURLRow.detailText = url.absoluteString;
requestURLRow.selectionFuture = ^{
UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
urlWebViewController.title = url.absoluteString;
return urlWebViewController;
};
[rows addObject:requestURLRow];
FLEXNetworkDetailRow *requestMethodRow = [FLEXNetworkDetailRow new];
requestMethodRow.title = @"Request Method";
requestMethodRow.detailText = transaction.request.HTTPMethod;
[rows addObject:requestMethodRow];
if (transaction.cachedRequestBody.length > 0) {
FLEXNetworkDetailRow *postBodySizeRow = [FLEXNetworkDetailRow new];
postBodySizeRow.title = @"Request Body Size";
postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.cachedRequestBody.length countStyle:NSByteCountFormatterCountStyleBinary];
[rows addObject:postBodySizeRow];
FLEXNetworkDetailRow *postBodyRow = [FLEXNetworkDetailRow new];
postBodyRow.title = @"Request Body";
postBodyRow.detailText = @"tap to view";
postBodyRow.selectionFuture = ^UIViewController * () {
// Show the body if we can
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
if (detailViewController) {
detailViewController.title = @"Request Body";
return detailViewController;
}
// We can't show the body, alert user
return [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Can't View HTTP Body Data");
make.message(@"FLEX does not have a viewer for request body data with MIME type: ");
make.message(contentType);
make.button(@"Dismiss").cancelStyle();
}];
};
[rows addObject:postBodyRow];
}
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
if (statusCodeString.length > 0) {
FLEXNetworkDetailRow *statusCodeRow = [FLEXNetworkDetailRow new];
statusCodeRow.title = @"Status Code";
statusCodeRow.detailText = statusCodeString;
[rows addObject:statusCodeRow];
}
if (transaction.error) {
FLEXNetworkDetailRow *errorRow = [FLEXNetworkDetailRow new];
errorRow.title = @"Error";
errorRow.detailText = transaction.error.localizedDescription;
[rows addObject:errorRow];
}
FLEXNetworkDetailRow *responseBodyRow = [FLEXNetworkDetailRow new];
responseBodyRow.title = @"Response Body";
NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
if (responseData.length > 0) {
responseBodyRow.detailText = @"tap to view";
// Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
__weak NSData *weakResponseData = responseData;
responseBodyRow.selectionFuture = ^UIViewController * () {
// Show the response if we can
NSString *contentType = transaction.response.MIMEType;
NSData *strongResponseData = weakResponseData;
if (strongResponseData) {
UIViewController *bodyDetailController = [self detailViewControllerForMIMEType:contentType data:strongResponseData];
if (bodyDetailController) {
bodyDetailController.title = @"Response";
return bodyDetailController;
}
}
// We can't show the response, alert user
return [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Unable to View Response");
if (strongResponseData) {
make.message(@"No viewer content type: ").message(contentType);
} else {
make.message(@"The response has been purged from the cache");
}
make.button(@"OK").cancelStyle();
}];
};
} else {
BOOL emptyResponse = transaction.receivedDataLength == 0;
responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
}
[rows addObject:responseBodyRow];
FLEXNetworkDetailRow *responseSizeRow = [FLEXNetworkDetailRow new];
responseSizeRow.title = @"Response Size";
responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
[rows addObject:responseSizeRow];
FLEXNetworkDetailRow *mimeTypeRow = [FLEXNetworkDetailRow new];
mimeTypeRow.title = @"MIME Type";
mimeTypeRow.detailText = transaction.response.MIMEType;
[rows addObject:mimeTypeRow];
FLEXNetworkDetailRow *mechanismRow = [FLEXNetworkDetailRow new];
mechanismRow.title = @"Mechanism";
mechanismRow.detailText = transaction.requestMechanism;
[rows addObject:mechanismRow];
NSDateFormatter *startTimeFormatter = [NSDateFormatter new];
startTimeFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
FLEXNetworkDetailRow *localStartTimeRow = [FLEXNetworkDetailRow new];
localStartTimeRow.title = [NSString stringWithFormat:@"Start Time (%@)", [[NSTimeZone localTimeZone] abbreviationForDate:transaction.startTime]];
localStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
[rows addObject:localStartTimeRow];
startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
FLEXNetworkDetailRow *utcStartTimeRow = [FLEXNetworkDetailRow new];
utcStartTimeRow.title = @"Start Time (UTC)";
utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
[rows addObject:utcStartTimeRow];
FLEXNetworkDetailRow *unixStartTime = [FLEXNetworkDetailRow new];
unixStartTime.title = @"Unix Start Time";
unixStartTime.detailText = [NSString stringWithFormat:@"%f", [transaction.startTime timeIntervalSince1970]];
[rows addObject:unixStartTime];
FLEXNetworkDetailRow *durationRow = [FLEXNetworkDetailRow new];
durationRow.title = @"Total Duration";
durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
[rows addObject:durationRow];
FLEXNetworkDetailRow *latencyRow = [FLEXNetworkDetailRow new];
latencyRow.title = @"Latency";
latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
[rows addObject:latencyRow];
FLEXNetworkDetailSection *generalSection = [FLEXNetworkDetailSection new];
generalSection.title = @"General";
generalSection.rows = rows;
return generalSection;
}
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *requestHeadersSection = [FLEXNetworkDetailSection new];
requestHeadersSection.title = @"Request Headers";
requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
return requestHeadersSection;
}
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *postBodySection = [FLEXNetworkDetailSection new];
postBodySection.title = @"Request Body Parameters";
if (transaction.cachedRequestBody.length > 0) {
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
NSString *bodyString = [NSString stringWithCString:[self postBodyDataForTransaction:transaction].bytes encoding:NSUTF8StringEncoding];
postBodySection.rows = [self networkDetailRowsFromQueryItems:[FLEXUtility itemsFromQueryString:bodyString]];
}
}
return postBodySection;
}
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
NSArray<NSURLQueryItem *> *queries = [FLEXUtility itemsFromQueryString:transaction.request.URL.query];
FLEXNetworkDetailSection *querySection = [FLEXNetworkDetailSection new];
querySection.title = @"Query Parameters";
querySection.rows = [self networkDetailRowsFromQueryItems:queries];
return querySection;
}
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *responseHeadersSection = [FLEXNetworkDetailSection new];
responseHeadersSection.title = @"Response Headers";
if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
}
return responseHeadersSection;
}
+ (NSArray<FLEXNetworkDetailRow *> *)networkDetailRowsFromDictionary:(NSDictionary<NSString *, id> *)dictionary
{
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
NSArray<NSString *> *sortedKeys = [dictionary.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
for (NSString *key in sortedKeys) {
id value = dictionary[key];
FLEXNetworkDetailRow *row = [FLEXNetworkDetailRow new];
row.title = key;
row.detailText = [value description];
[rows addObject:row];
}
return rows.copy;
}
+ (NSArray<FLEXNetworkDetailRow *> *)networkDetailRowsFromQueryItems:(NSArray<NSURLQueryItem *> *)items
{
// Sort the items by name
items = [items sortedArrayUsingComparator:^NSComparisonResult(NSURLQueryItem *item1, NSURLQueryItem *item2) {
return [item1.name caseInsensitiveCompare:item2.name];
}];
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
for (NSURLQueryItem *item in items) {
FLEXNetworkDetailRow *row = [FLEXNetworkDetailRow new];
row.title = item.name;
row.detailText = item.value;
[rows addObject:row];
}
return [rows copy];
}
+ (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data
{
FLEXCustomContentViewerFuture makeCustomViewer = [FLEXManager sharedManager].customContentTypeViewers[mimeType.lowercaseString];
if (makeCustomViewer) {
UIViewController *viewer = makeCustomViewer(data);
if (viewer) {
return viewer;
}
}
// FIXME (RKO): Don't rely on UTF8 string encoding
UIViewController *detailViewController = nil;
if ([FLEXUtility isValidJSONData:data]) {
NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
if (prettyJSON.length > 0) {
detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
}
} else if ([mimeType hasPrefix:@"image/"]) {
UIImage *image = [UIImage imageWithData:data];
detailViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else if ([mimeType isEqual:@"application/x-plist"]) {
id propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
detailViewController = [[FLEXWebViewController alloc] initWithText:[propertyList description]];
}
// Fall back to trying to show the response as text
if (!detailViewController) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text.length > 0) {
detailViewController = [[FLEXWebViewController alloc] initWithText:text];
}
}
return detailViewController;
}
+ (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction
{
NSData *bodyData = transaction.cachedRequestBody;
if (bodyData.length > 0) {
NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
if ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0) {
bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
}
}
return bodyData;
}
@end