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