FLEXNetworkTransactionDetailTableViewController.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. //
  2. // FLEXNetworkTransactionDetailTableViewController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 2/10/15.
  6. // Copyright (c) 2015 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXNetworkTransactionDetailTableViewController.h"
  9. #import "FLEXNetworkRecorder.h"
  10. #import "FLEXNetworkTransaction.h"
  11. #import "FLEXWebViewController.h"
  12. #import "FLEXImagePreviewViewController.h"
  13. #import "FLEXMultilineTableViewCell.h"
  14. #import "FLEXUtility.h"
  15. @interface FLEXNetworkDetailSection : NSObject
  16. @property (nonatomic, copy) NSString *title;
  17. @property (nonatomic, copy) NSArray *rows;
  18. @end
  19. @implementation FLEXNetworkDetailSection
  20. @end
  21. typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
  22. @interface FLEXNetworkDetailRow : NSObject
  23. @property (nonatomic, copy) NSString *title;
  24. @property (nonatomic, copy) NSString *detailText;
  25. @property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
  26. @end
  27. @implementation FLEXNetworkDetailRow
  28. @end
  29. @interface FLEXNetworkTransactionDetailTableViewController ()
  30. @property (nonatomic, copy) NSArray *sections;
  31. @end
  32. @implementation FLEXNetworkTransactionDetailTableViewController
  33. - (instancetype)initWithStyle:(UITableViewStyle)style
  34. {
  35. // Force grouped style
  36. self = [super initWithStyle:UITableViewStyleGrouped];
  37. if (self) {
  38. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
  39. }
  40. return self;
  41. }
  42. - (void)viewDidLoad
  43. {
  44. [super viewDidLoad];
  45. [self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineTableViewCellIdentifier];
  46. }
  47. - (void)setTransaction:(FLEXNetworkTransaction *)transaction
  48. {
  49. if (![_transaction isEqual:transaction]) {
  50. _transaction = transaction;
  51. self.title = [transaction.request.URL lastPathComponent];
  52. [self rebuildTableSections];
  53. }
  54. }
  55. - (void)setSections:(NSArray *)sections
  56. {
  57. if (![_sections isEqual:sections]) {
  58. _sections = [sections copy];
  59. [self.tableView reloadData];
  60. }
  61. }
  62. - (void)rebuildTableSections
  63. {
  64. NSMutableArray *sections = [NSMutableArray array];
  65. FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
  66. if ([generalSection.rows count] > 0) {
  67. [sections addObject:generalSection];
  68. }
  69. FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
  70. if ([requestHeadersSection.rows count] > 0) {
  71. [sections addObject:requestHeadersSection];
  72. }
  73. FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
  74. if ([queryParametersSection.rows count] > 0) {
  75. [sections addObject:queryParametersSection];
  76. }
  77. FLEXNetworkDetailSection *postBodySection = [[self class] postBodySectionForTransaction:self.transaction];
  78. if ([postBodySection.rows count] > 0) {
  79. [sections addObject:postBodySection];
  80. }
  81. FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
  82. if ([responseHeadersSection.rows count] > 0) {
  83. [sections addObject:responseHeadersSection];
  84. }
  85. self.sections = sections;
  86. }
  87. - (void)handleTransactionUpdatedNotification:(NSNotification *)notification
  88. {
  89. FLEXNetworkTransaction *transaction = [[notification userInfo] objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
  90. if (transaction == self.transaction) {
  91. [self rebuildTableSections];
  92. }
  93. }
  94. #pragma mark - Table view data source
  95. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  96. {
  97. return [self.sections count];
  98. }
  99. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  100. {
  101. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
  102. return [sectionModel.rows count];
  103. }
  104. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  105. {
  106. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
  107. return sectionModel.title;
  108. }
  109. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  110. {
  111. FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineTableViewCellIdentifier forIndexPath:indexPath];
  112. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  113. cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
  114. cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
  115. cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
  116. return cell;
  117. }
  118. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  119. {
  120. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  121. UIViewController *viewControllerToPush = nil;
  122. if (rowModel.selectionFuture) {
  123. viewControllerToPush = rowModel.selectionFuture();
  124. }
  125. if (viewControllerToPush) {
  126. [self.navigationController pushViewController:viewControllerToPush animated:YES];
  127. }
  128. [tableView deselectRowAtIndexPath:indexPath animated:YES];
  129. }
  130. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  131. {
  132. FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
  133. NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
  134. BOOL showsAccessory = row.selectionFuture != nil;
  135. return [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.bounds.size.width style:UITableViewStyleGrouped showsAccessory:showsAccessory];
  136. }
  137. - (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath
  138. {
  139. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:indexPath.section];
  140. return [sectionModel.rows objectAtIndex:indexPath.row];
  141. }
  142. #pragma mark - View Configuration
  143. + (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row
  144. {
  145. NSDictionary *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
  146. NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
  147. NSDictionary *detailAttributes = @{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont],
  148. NSForegroundColorAttributeName : [UIColor blackColor] };
  149. NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
  150. NSString *detailText = row.detailText ?: @"";
  151. NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
  152. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
  153. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
  154. return attributedText;
  155. }
  156. #pragma mark - Table Data Generation
  157. + (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction
  158. {
  159. NSMutableArray *rows = [NSMutableArray array];
  160. FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
  161. requestURLRow.title = @"Request URL";
  162. NSURL *url = transaction.request.URL;
  163. requestURLRow.detailText = url.absoluteString;
  164. requestURLRow.selectionFuture = ^{
  165. UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
  166. urlWebViewController.title = url.absoluteString;
  167. return urlWebViewController;
  168. };
  169. [rows addObject:requestURLRow];
  170. FLEXNetworkDetailRow *responseBodyRow = [[FLEXNetworkDetailRow alloc] init];
  171. responseBodyRow.title = @"Response Body";
  172. NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
  173. if ([responseData length] > 0) {
  174. responseBodyRow.detailText = @"tap to view";
  175. // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
  176. __weak NSData *weakResponseData = responseData;
  177. responseBodyRow.selectionFuture = ^{
  178. UIViewController *responseBodyDetailViewController = nil;
  179. NSData *strongResponseData = weakResponseData;
  180. if (strongResponseData) {
  181. responseBodyDetailViewController = [self detailViewControllerForMIMEType:transaction.response.MIMEType data:strongResponseData];
  182. responseBodyDetailViewController.title = @"Response";
  183. } else {
  184. // FIXME (RKO): Show an alert explaining that the data was purged?
  185. }
  186. return responseBodyDetailViewController;
  187. };
  188. } else {
  189. BOOL emptyResponse = transaction.receivedDataLength == 0;
  190. responseBodyRow.detailText = emptyResponse ? @"empty" : @"purged from cache";
  191. }
  192. [rows addObject:responseBodyRow];
  193. if (transaction.error) {
  194. FLEXNetworkDetailRow *errorRow = [[FLEXNetworkDetailRow alloc] init];
  195. errorRow.title = @"Error";
  196. errorRow.detailText = transaction.error.localizedDescription;
  197. [rows addObject:errorRow];
  198. }
  199. FLEXNetworkDetailRow *requestMethodRow = [[FLEXNetworkDetailRow alloc] init];
  200. requestMethodRow.title = @"Request Method";
  201. requestMethodRow.detailText = transaction.request.HTTPMethod;
  202. [rows addObject:requestMethodRow];
  203. if ([transaction.request.HTTPBody length] > 0) {
  204. FLEXNetworkDetailRow *postBodySizeRow = [[FLEXNetworkDetailRow alloc] init];
  205. postBodySizeRow.title = @"POST Body Size";
  206. postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:[transaction.request.HTTPBody length] countStyle:NSByteCountFormatterCountStyleBinary];
  207. [rows addObject:postBodySizeRow];
  208. FLEXNetworkDetailRow *postBodyRow = [[FLEXNetworkDetailRow alloc] init];
  209. postBodyRow.title = @"POST Body";
  210. postBodyRow.detailText = @"tap to view";
  211. postBodyRow.selectionFuture = ^{
  212. NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
  213. UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
  214. detailViewController.title = @"POST Body";
  215. return detailViewController;
  216. };
  217. [rows addObject:postBodyRow];
  218. }
  219. NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
  220. if ([statusCodeString length] > 0) {
  221. FLEXNetworkDetailRow *statusCodeRow = [[FLEXNetworkDetailRow alloc] init];
  222. statusCodeRow.title = @"Status Code";
  223. statusCodeRow.detailText = statusCodeString;
  224. [rows addObject:statusCodeRow];
  225. }
  226. FLEXNetworkDetailRow *mechanismRow = [[FLEXNetworkDetailRow alloc] init];
  227. mechanismRow.title = @"Mechanism";
  228. mechanismRow.detailText = transaction.requestMechanism;
  229. [rows addObject:mechanismRow];
  230. FLEXNetworkDetailRow *mimeTypeRow = [[FLEXNetworkDetailRow alloc] init];
  231. mimeTypeRow.title = @"MIME Type";
  232. mimeTypeRow.detailText = transaction.response.MIMEType;
  233. [rows addObject:mimeTypeRow];
  234. FLEXNetworkDetailRow *responseSizeRow = [[FLEXNetworkDetailRow alloc] init];
  235. responseSizeRow.title = @"Response Size";
  236. responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
  237. [rows addObject:responseSizeRow];
  238. NSDateFormatter *startTimeFormatter = [[NSDateFormatter alloc] init];
  239. startTimeFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
  240. FLEXNetworkDetailRow *localStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
  241. localStartTimeRow.title = [NSString stringWithFormat:@"Start Time (%@)", [[NSTimeZone localTimeZone] abbreviationForDate:transaction.startTime]];
  242. localStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
  243. [rows addObject:localStartTimeRow];
  244. startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
  245. FLEXNetworkDetailRow *utcStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
  246. utcStartTimeRow.title = @"Start Time (UTC)";
  247. utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
  248. [rows addObject:utcStartTimeRow];
  249. FLEXNetworkDetailRow *unixStartTime = [[FLEXNetworkDetailRow alloc] init];
  250. unixStartTime.title = @"Unix Start Time";
  251. unixStartTime.detailText = [NSString stringWithFormat:@"%f", [transaction.startTime timeIntervalSince1970]];
  252. [rows addObject:unixStartTime];
  253. FLEXNetworkDetailRow *durationRow = [[FLEXNetworkDetailRow alloc] init];
  254. durationRow.title = @"Total Duration";
  255. durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
  256. [rows addObject:durationRow];
  257. FLEXNetworkDetailRow *latencyRow = [[FLEXNetworkDetailRow alloc] init];
  258. latencyRow.title = @"Latency";
  259. latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
  260. [rows addObject:latencyRow];
  261. FLEXNetworkDetailSection *generalSection = [[FLEXNetworkDetailSection alloc] init];
  262. generalSection.title = @"General";
  263. generalSection.rows = rows;
  264. return generalSection;
  265. }
  266. + (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  267. {
  268. FLEXNetworkDetailSection *requestHeadersSection = [[FLEXNetworkDetailSection alloc] init];
  269. requestHeadersSection.title = @"Request Headers";
  270. requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
  271. return requestHeadersSection;
  272. }
  273. + (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction
  274. {
  275. FLEXNetworkDetailSection *postBodySection = [[FLEXNetworkDetailSection alloc] init];
  276. postBodySection.title = @"POST Parameters";
  277. if ([transaction.request.HTTPBody length] > 0) {
  278. NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
  279. if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
  280. NSString *bodyString = [[NSString alloc] initWithData:[self postBodyDataForTransaction:transaction] encoding:NSUTF8StringEncoding];
  281. postBodySection.rows = [self networkDetailRowsFromDictionary:[FLEXUtility dictionaryFromQuery:bodyString]];
  282. }
  283. }
  284. return postBodySection;
  285. }
  286. + (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  287. {
  288. NSDictionary *queryDictionary = [FLEXUtility dictionaryFromQuery:transaction.request.URL.query];
  289. FLEXNetworkDetailSection *querySection = [[FLEXNetworkDetailSection alloc] init];
  290. querySection.title = @"Query Parameters";
  291. querySection.rows = [self networkDetailRowsFromDictionary:queryDictionary];
  292. return querySection;
  293. }
  294. + (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  295. {
  296. FLEXNetworkDetailSection *responseHeadersSection = [[FLEXNetworkDetailSection alloc] init];
  297. responseHeadersSection.title = @"Response Headers";
  298. if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
  299. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
  300. responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
  301. }
  302. return responseHeadersSection;
  303. }
  304. + (NSArray *)networkDetailRowsFromDictionary:(NSDictionary *)dictionary
  305. {
  306. NSMutableArray *rows = [NSMutableArray arrayWithCapacity:[dictionary count]];
  307. NSArray *sortedKeys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
  308. for (NSString *key in sortedKeys) {
  309. NSString *value = [dictionary objectForKey:key];
  310. FLEXNetworkDetailRow *row = [[FLEXNetworkDetailRow alloc] init];
  311. row.title = key;
  312. row.detailText = [value description];
  313. [rows addObject:row];
  314. }
  315. return [rows copy];
  316. }
  317. + (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data
  318. {
  319. // FIXME (RKO): Don't rely on UTF8 string encoding
  320. UIViewController *detailViewController = nil;
  321. if ([mimeType isEqual:@"application/json"]) {
  322. NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
  323. if ([prettyJSON length] > 0) {
  324. detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
  325. }
  326. } else if ([mimeType hasPrefix:@"image/"]) {
  327. UIImage *image = [UIImage imageWithData:data];
  328. detailViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
  329. } else if ([mimeType isEqual:@"application/x-plist"]) {
  330. id propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
  331. detailViewController = [[FLEXWebViewController alloc] initWithText:[propertyList description]];
  332. } else {
  333. NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  334. if ([text length] > 0) {
  335. detailViewController = [[FLEXWebViewController alloc] initWithText:text];
  336. }
  337. }
  338. return detailViewController;
  339. }
  340. + (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction
  341. {
  342. NSData *bodyData = transaction.request.HTTPBody;
  343. if ([bodyData length] > 0) {
  344. NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
  345. if ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0) {
  346. bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
  347. }
  348. }
  349. return bodyData;
  350. }
  351. @end