FLEXNetworkTransactionDetailController.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. //
  2. // FLEXNetworkTransactionDetailController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 2/10/15.
  6. // Copyright (c) 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXColor.h"
  9. #import "FLEXNetworkTransactionDetailController.h"
  10. #import "FLEXNetworkCurlLogger.h"
  11. #import "FLEXNetworkRecorder.h"
  12. #import "FLEXNetworkTransaction.h"
  13. #import "FLEXWebViewController.h"
  14. #import "FLEXImagePreviewViewController.h"
  15. #import "FLEXMultilineTableViewCell.h"
  16. #import "FLEXUtility.h"
  17. #import "FLEXManager+Private.h"
  18. #import "FLEXTableView.h"
  19. #import "UIBarButtonItem+FLEX.h"
  20. typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
  21. @interface FLEXNetworkDetailRow : NSObject
  22. @property (nonatomic, copy) NSString *title;
  23. @property (nonatomic, copy) NSString *detailText;
  24. @property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
  25. @end
  26. @implementation FLEXNetworkDetailRow
  27. @end
  28. @interface FLEXNetworkDetailSection : NSObject
  29. @property (nonatomic, copy) NSString *title;
  30. @property (nonatomic, copy) NSArray<FLEXNetworkDetailRow *> *rows;
  31. @end
  32. @implementation FLEXNetworkDetailSection
  33. @end
  34. @interface FLEXNetworkTransactionDetailController ()
  35. @property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
  36. @end
  37. @implementation FLEXNetworkTransactionDetailController
  38. - (instancetype)initWithStyle:(UITableViewStyle)style {
  39. // Force grouped style
  40. self = [super initWithStyle:UITableViewStyleGrouped];
  41. if (self) {
  42. [NSNotificationCenter.defaultCenter addObserver:self
  43. selector:@selector(handleTransactionUpdatedNotification:)
  44. name:kFLEXNetworkRecorderTransactionUpdatedNotification
  45. object:nil
  46. ];
  47. #if !TARGET_OS_TV
  48. self.toolbarItems = @[
  49. UIBarButtonItem.flex_flexibleSpace,
  50. [UIBarButtonItem
  51. flex_itemWithTitle:@"Copy curl"
  52. target:self
  53. action:@selector(copyButtonPressed:)
  54. ]
  55. ];
  56. #endif
  57. }
  58. return self;
  59. }
  60. - (void)viewDidLoad {
  61. [super viewDidLoad];
  62. [self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineCell];
  63. }
  64. - (void)setTransaction:(FLEXNetworkTransaction *)transaction {
  65. if (![_transaction isEqual:transaction]) {
  66. _transaction = transaction;
  67. self.title = [transaction.request.URL lastPathComponent];
  68. [self rebuildTableSections];
  69. }
  70. }
  71. - (void)setSections:(NSArray<FLEXNetworkDetailSection *> *)sections {
  72. if (![_sections isEqual:sections]) {
  73. _sections = [sections copy];
  74. [self.tableView reloadData];
  75. }
  76. }
  77. - (void)rebuildTableSections {
  78. NSMutableArray<FLEXNetworkDetailSection *> *sections = [NSMutableArray new];
  79. FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
  80. if (generalSection.rows.count > 0) {
  81. [sections addObject:generalSection];
  82. }
  83. FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
  84. if (requestHeadersSection.rows.count > 0) {
  85. [sections addObject:requestHeadersSection];
  86. }
  87. FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
  88. if (queryParametersSection.rows.count > 0) {
  89. [sections addObject:queryParametersSection];
  90. }
  91. FLEXNetworkDetailSection *postBodySection = [[self class] postBodySectionForTransaction:self.transaction];
  92. if (postBodySection.rows.count > 0) {
  93. [sections addObject:postBodySection];
  94. }
  95. FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
  96. if (responseHeadersSection.rows.count > 0) {
  97. [sections addObject:responseHeadersSection];
  98. }
  99. self.sections = sections;
  100. }
  101. - (void)handleTransactionUpdatedNotification:(NSNotification *)notification {
  102. FLEXNetworkTransaction *transaction = [[notification userInfo] objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
  103. if (transaction == self.transaction) {
  104. [self rebuildTableSections];
  105. }
  106. }
  107. - (void)copyButtonPressed:(id)sender {
  108. #if !TARGET_OS_TV
  109. [UIPasteboard.generalPasteboard setString:[FLEXNetworkCurlLogger curlCommandString:_transaction.request]];
  110. #endif
  111. }
  112. #pragma mark - Table view data source
  113. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  114. return self.sections.count;
  115. }
  116. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  117. FLEXNetworkDetailSection *sectionModel = self.sections[section];
  118. return sectionModel.rows.count;
  119. }
  120. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
  121. FLEXNetworkDetailSection *sectionModel = self.sections[section];
  122. return sectionModel.title;
  123. }
  124. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  125. FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineCell forIndexPath:indexPath];
  126. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  127. cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
  128. cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
  129. cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
  130. return cell;
  131. }
  132. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  133. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  134. UIViewController *viewController = nil;
  135. if (rowModel.selectionFuture) {
  136. viewController = rowModel.selectionFuture();
  137. }
  138. if ([viewController isKindOfClass:UIAlertController.class]) {
  139. [self presentViewController:viewController animated:YES completion:nil];
  140. } else if (viewController) {
  141. [self.navigationController pushViewController:viewController animated:YES];
  142. }
  143. [tableView deselectRowAtIndexPath:indexPath animated:YES];
  144. }
  145. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  146. FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
  147. NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
  148. BOOL showsAccessory = row.selectionFuture != nil;
  149. return [FLEXMultilineTableViewCell
  150. preferredHeightWithAttributedText:attributedText
  151. maxWidth:tableView.bounds.size.width
  152. style:tableView.style
  153. showsAccessory:showsAccessory
  154. ];
  155. }
  156. - (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath {
  157. FLEXNetworkDetailSection *sectionModel = self.sections[indexPath.section];
  158. return sectionModel.rows[indexPath.row];
  159. }
  160. #pragma mark - Cell Copying
  161. - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
  162. return YES;
  163. }
  164. - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
  165. return action == @selector(copy:);
  166. }
  167. - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
  168. if (action == @selector(copy:)) {
  169. FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
  170. #if !TARGET_OS_TV
  171. UIPasteboard.generalPasteboard.string = row.detailText;
  172. #endif
  173. }
  174. }
  175. #if FLEX_AT_LEAST_IOS13_SDK
  176. #if !TARGET_OS_TV
  177. - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
  178. return [UIContextMenuConfiguration
  179. configurationWithIdentifier:nil
  180. previewProvider:nil
  181. actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
  182. UIAction *copy = [UIAction
  183. actionWithTitle:@"Copy"
  184. image:nil
  185. identifier:nil
  186. handler:^(__kindof UIAction *action) {
  187. FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
  188. UIPasteboard.generalPasteboard.string = row.detailText;
  189. }
  190. ];
  191. return [UIMenu
  192. menuWithTitle:@"" image:nil identifier:nil
  193. options:UIMenuOptionsDisplayInline
  194. children:@[copy]
  195. ];
  196. }
  197. ];
  198. }
  199. #endif
  200. #endif
  201. #pragma mark - View Configuration
  202. + (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row {
  203. NSDictionary<NSString *, id> *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
  204. NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
  205. NSDictionary<NSString *, id> *detailAttributes = @{ NSFontAttributeName : UIFont.flex_defaultTableCellFont,
  206. NSForegroundColorAttributeName : FLEXColor.primaryTextColor };
  207. NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
  208. NSString *detailText = row.detailText ?: @"";
  209. NSMutableAttributedString *attributedText = [NSMutableAttributedString new];
  210. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
  211. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
  212. return attributedText;
  213. }
  214. #pragma mark - Table Data Generation
  215. + (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction {
  216. NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
  217. FLEXNetworkDetailRow *requestURLRow = [FLEXNetworkDetailRow new];
  218. requestURLRow.title = @"Request URL";
  219. NSURL *url = transaction.request.URL;
  220. requestURLRow.detailText = url.absoluteString;
  221. requestURLRow.selectionFuture = ^{
  222. UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
  223. urlWebViewController.title = url.absoluteString;
  224. return urlWebViewController;
  225. };
  226. [rows addObject:requestURLRow];
  227. FLEXNetworkDetailRow *requestMethodRow = [FLEXNetworkDetailRow new];
  228. requestMethodRow.title = @"Request Method";
  229. requestMethodRow.detailText = transaction.request.HTTPMethod;
  230. [rows addObject:requestMethodRow];
  231. if (transaction.cachedRequestBody.length > 0) {
  232. FLEXNetworkDetailRow *postBodySizeRow = [FLEXNetworkDetailRow new];
  233. postBodySizeRow.title = @"Request Body Size";
  234. postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.cachedRequestBody.length countStyle:NSByteCountFormatterCountStyleBinary];
  235. [rows addObject:postBodySizeRow];
  236. FLEXNetworkDetailRow *postBodyRow = [FLEXNetworkDetailRow new];
  237. postBodyRow.title = @"Request Body";
  238. postBodyRow.detailText = @"tap to view";
  239. postBodyRow.selectionFuture = ^UIViewController * () {
  240. // Show the body if we can
  241. NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
  242. UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
  243. if (detailViewController) {
  244. detailViewController.title = @"Request Body";
  245. return detailViewController;
  246. }
  247. // We can't show the body, alert user
  248. return [FLEXAlert makeAlert:^(FLEXAlert *make) {
  249. make.title(@"Can't View HTTP Body Data");
  250. make.message(@"FLEX does not have a viewer for request body data with MIME type: ");
  251. make.message(contentType);
  252. make.button(@"Dismiss").cancelStyle();
  253. }];
  254. };
  255. [rows addObject:postBodyRow];
  256. }
  257. NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
  258. if (statusCodeString.length > 0) {
  259. FLEXNetworkDetailRow *statusCodeRow = [FLEXNetworkDetailRow new];
  260. statusCodeRow.title = @"Status Code";
  261. statusCodeRow.detailText = statusCodeString;
  262. [rows addObject:statusCodeRow];
  263. }
  264. if (transaction.error) {
  265. FLEXNetworkDetailRow *errorRow = [FLEXNetworkDetailRow new];
  266. errorRow.title = @"Error";
  267. errorRow.detailText = transaction.error.localizedDescription;
  268. [rows addObject:errorRow];
  269. }
  270. FLEXNetworkDetailRow *responseBodyRow = [FLEXNetworkDetailRow new];
  271. responseBodyRow.title = @"Response Body";
  272. NSData *responseData = [FLEXNetworkRecorder.defaultRecorder cachedResponseBodyForTransaction:transaction];
  273. if (responseData.length > 0) {
  274. responseBodyRow.detailText = @"tap to view";
  275. // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
  276. __weak NSData *weakResponseData = responseData;
  277. responseBodyRow.selectionFuture = ^UIViewController * () {
  278. // Show the response if we can
  279. NSString *contentType = transaction.response.MIMEType;
  280. NSData *strongResponseData = weakResponseData;
  281. if (strongResponseData) {
  282. UIViewController *bodyDetailController = [self detailViewControllerForMIMEType:contentType data:strongResponseData];
  283. if (bodyDetailController) {
  284. bodyDetailController.title = @"Response";
  285. return bodyDetailController;
  286. }
  287. }
  288. // We can't show the response, alert user
  289. return [FLEXAlert makeAlert:^(FLEXAlert *make) {
  290. make.title(@"Unable to View Response");
  291. if (strongResponseData) {
  292. make.message(@"No viewer content type: ").message(contentType);
  293. } else {
  294. make.message(@"The response has been purged from the cache");
  295. }
  296. make.button(@"OK").cancelStyle();
  297. }];
  298. };
  299. } else {
  300. BOOL emptyResponse = transaction.receivedDataLength == 0;
  301. responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
  302. }
  303. [rows addObject:responseBodyRow];
  304. FLEXNetworkDetailRow *responseSizeRow = [FLEXNetworkDetailRow new];
  305. responseSizeRow.title = @"Response Size";
  306. responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
  307. [rows addObject:responseSizeRow];
  308. FLEXNetworkDetailRow *mimeTypeRow = [FLEXNetworkDetailRow new];
  309. mimeTypeRow.title = @"MIME Type";
  310. mimeTypeRow.detailText = transaction.response.MIMEType;
  311. [rows addObject:mimeTypeRow];
  312. FLEXNetworkDetailRow *mechanismRow = [FLEXNetworkDetailRow new];
  313. mechanismRow.title = @"Mechanism";
  314. mechanismRow.detailText = transaction.requestMechanism;
  315. [rows addObject:mechanismRow];
  316. NSDateFormatter *startTimeFormatter = [NSDateFormatter new];
  317. startTimeFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
  318. FLEXNetworkDetailRow *localStartTimeRow = [FLEXNetworkDetailRow new];
  319. localStartTimeRow.title = [NSString stringWithFormat:@"Start Time (%@)", [NSTimeZone.localTimeZone abbreviationForDate:transaction.startTime]];
  320. localStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
  321. [rows addObject:localStartTimeRow];
  322. startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
  323. FLEXNetworkDetailRow *utcStartTimeRow = [FLEXNetworkDetailRow new];
  324. utcStartTimeRow.title = @"Start Time (UTC)";
  325. utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
  326. [rows addObject:utcStartTimeRow];
  327. FLEXNetworkDetailRow *unixStartTime = [FLEXNetworkDetailRow new];
  328. unixStartTime.title = @"Unix Start Time";
  329. unixStartTime.detailText = [NSString stringWithFormat:@"%f", [transaction.startTime timeIntervalSince1970]];
  330. [rows addObject:unixStartTime];
  331. FLEXNetworkDetailRow *durationRow = [FLEXNetworkDetailRow new];
  332. durationRow.title = @"Total Duration";
  333. durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
  334. [rows addObject:durationRow];
  335. FLEXNetworkDetailRow *latencyRow = [FLEXNetworkDetailRow new];
  336. latencyRow.title = @"Latency";
  337. latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
  338. [rows addObject:latencyRow];
  339. FLEXNetworkDetailSection *generalSection = [FLEXNetworkDetailSection new];
  340. generalSection.title = @"General";
  341. generalSection.rows = rows;
  342. return generalSection;
  343. }
  344. + (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
  345. FLEXNetworkDetailSection *requestHeadersSection = [FLEXNetworkDetailSection new];
  346. requestHeadersSection.title = @"Request Headers";
  347. requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
  348. return requestHeadersSection;
  349. }
  350. + (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction {
  351. FLEXNetworkDetailSection *postBodySection = [FLEXNetworkDetailSection new];
  352. postBodySection.title = @"Request Body Parameters";
  353. if (transaction.cachedRequestBody.length > 0) {
  354. NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
  355. if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
  356. NSData *body = [self postBodyDataForTransaction:transaction];
  357. NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
  358. postBodySection.rows = [self networkDetailRowsFromQueryItems:[FLEXUtility itemsFromQueryString:bodyString]];
  359. }
  360. }
  361. return postBodySection;
  362. }
  363. + (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
  364. NSArray<NSURLQueryItem *> *queries = [FLEXUtility itemsFromQueryString:transaction.request.URL.query];
  365. FLEXNetworkDetailSection *querySection = [FLEXNetworkDetailSection new];
  366. querySection.title = @"Query Parameters";
  367. querySection.rows = [self networkDetailRowsFromQueryItems:queries];
  368. return querySection;
  369. }
  370. + (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
  371. FLEXNetworkDetailSection *responseHeadersSection = [FLEXNetworkDetailSection new];
  372. responseHeadersSection.title = @"Response Headers";
  373. if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
  374. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
  375. responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
  376. }
  377. return responseHeadersSection;
  378. }
  379. + (NSArray<FLEXNetworkDetailRow *> *)networkDetailRowsFromDictionary:(NSDictionary<NSString *, id> *)dictionary {
  380. NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
  381. NSArray<NSString *> *sortedKeys = [dictionary.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
  382. for (NSString *key in sortedKeys) {
  383. id value = dictionary[key];
  384. FLEXNetworkDetailRow *row = [FLEXNetworkDetailRow new];
  385. row.title = key;
  386. row.detailText = [value description];
  387. [rows addObject:row];
  388. }
  389. return rows.copy;
  390. }
  391. + (NSArray<FLEXNetworkDetailRow *> *)networkDetailRowsFromQueryItems:(NSArray<NSURLQueryItem *> *)items {
  392. // Sort the items by name
  393. items = [items sortedArrayUsingComparator:^NSComparisonResult(NSURLQueryItem *item1, NSURLQueryItem *item2) {
  394. return [item1.name caseInsensitiveCompare:item2.name];
  395. }];
  396. NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
  397. for (NSURLQueryItem *item in items) {
  398. FLEXNetworkDetailRow *row = [FLEXNetworkDetailRow new];
  399. row.title = item.name;
  400. row.detailText = item.value;
  401. [rows addObject:row];
  402. }
  403. return [rows copy];
  404. }
  405. + (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data {
  406. FLEXCustomContentViewerFuture makeCustomViewer = FLEXManager.sharedManager.customContentTypeViewers[mimeType.lowercaseString];
  407. if (makeCustomViewer) {
  408. UIViewController *viewer = makeCustomViewer(data);
  409. if (viewer) {
  410. return viewer;
  411. }
  412. }
  413. // FIXME (RKO): Don't rely on UTF8 string encoding
  414. UIViewController *detailViewController = nil;
  415. if ([FLEXUtility isValidJSONData:data]) {
  416. NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
  417. if (prettyJSON.length > 0) {
  418. detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
  419. }
  420. } else if ([mimeType hasPrefix:@"image/"]) {
  421. UIImage *image = [UIImage imageWithData:data];
  422. detailViewController = [FLEXImagePreviewViewController forImage:image];
  423. } else if ([mimeType isEqual:@"application/x-plist"]) {
  424. id propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
  425. detailViewController = [[FLEXWebViewController alloc] initWithText:[propertyList description]];
  426. }
  427. // Fall back to trying to show the response as text
  428. if (!detailViewController) {
  429. NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  430. if (text.length > 0) {
  431. detailViewController = [[FLEXWebViewController alloc] initWithText:text];
  432. }
  433. }
  434. return detailViewController;
  435. }
  436. + (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction {
  437. NSData *bodyData = transaction.cachedRequestBody;
  438. if (bodyData.length > 0) {
  439. NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
  440. if ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0) {
  441. bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
  442. }
  443. }
  444. return bodyData;
  445. }
  446. @end