added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
//
// FLEXFirebaseTransaction.m
// FLEX
//
// Created by Tanner Bennett on 12/24/21.
//
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import <dlfcn.h>
#include <string>
typedef std::string (*ReturnsString)(void *);
@implementation FLEXFirebaseSetDataInfo
+ (instancetype)data:(NSDictionary *)data merge:(NSNumber *)merge mergeFields:(NSArray *)mergeFields {
NSParameterAssert(data);
NSParameterAssert(merge || mergeFields);
FLEXFirebaseSetDataInfo *info = [self new];
info->_documentData = data;
info->_merge = merge;
info->_mergeFields = mergeFields;
return info;
}
@end
static NSString *FLEXStringFromFIRRequestType(FLEXFIRRequestType type) {
switch (type) {
case FLEXFIRRequestTypeNotFirebase:
return @"not firebase";
case FLEXFIRRequestTypeFetchQuery:
return @"query fetch";
case FLEXFIRRequestTypeFetchDocument:
return @"document fetch";
case FLEXFIRRequestTypeSetData:
return @"set data";
case FLEXFIRRequestTypeUpdateData:
return @"update data";
case FLEXFIRRequestTypeAddDocument:
return @"create";
case FLEXFIRRequestTypeDeleteDocument:
return @"delete";
}
return nil;
}
static FLEXFIRTransactionDirection FIRDirectionFromRequestType(FLEXFIRRequestType type) {
switch (type) {
case FLEXFIRRequestTypeNotFirebase:
return FLEXFIRTransactionDirectionNone;
case FLEXFIRRequestTypeFetchQuery:
case FLEXFIRRequestTypeFetchDocument:
return FLEXFIRTransactionDirectionPull;
case FLEXFIRRequestTypeSetData:
case FLEXFIRRequestTypeUpdateData:
case FLEXFIRRequestTypeAddDocument:
case FLEXFIRRequestTypeDeleteDocument:
return FLEXFIRTransactionDirectionPush;
}
return FLEXFIRTransactionDirectionNone;
}
@interface FLEXFirebaseTransaction ()
@property (nonatomic) id extraData;
@property (nonatomic, readonly) NSString *queryDescription;
@end
@implementation FLEXFirebaseTransaction
@synthesize queryDescription = _queryDescription;
+ (instancetype)initiator:(id)initiator requestType:(FLEXFIRRequestType)type extraData:(id)data {
FLEXFirebaseTransaction *fire = [FLEXFirebaseTransaction withStartTime:NSDate.date];
fire->_direction = FIRDirectionFromRequestType(type);
fire->_initiator = initiator;
fire->_requestType = type;
fire->_extraData = data;
return fire;
}
+ (instancetype)queryFetch:(FIRQuery *)initiator {
return [self initiator:initiator requestType:FLEXFIRRequestTypeFetchQuery extraData:nil];
}
+ (instancetype)documentFetch:(FIRDocumentReference *)initiator {
return [self initiator:initiator requestType:FLEXFIRRequestTypeFetchDocument extraData:nil];
}
+ (instancetype)setData:(FIRDocumentReference *)initiator data:(NSDictionary *)data
merge:(NSNumber *)merge mergeFields:(NSArray *)mergeFields {
FLEXFirebaseSetDataInfo *info = [FLEXFirebaseSetDataInfo data:data merge:merge mergeFields:mergeFields];
return [self initiator:initiator requestType:FLEXFIRRequestTypeSetData extraData:info];
}
+ (instancetype)updateData:(FIRDocumentReference *)initiator data:(NSDictionary *)data {
return [self initiator:initiator requestType:FLEXFIRRequestTypeUpdateData extraData:data];
}
+ (instancetype)addDocument:(FIRCollectionReference *)initiator document:(FIRDocumentReference *)doc {
return [self initiator:initiator requestType:FLEXFIRRequestTypeAddDocument extraData:doc];
}
+ (instancetype)deleteDocument:(FIRDocumentReference *)initiator {
return [self initiator:initiator requestType:FLEXFIRRequestTypeDeleteDocument extraData:nil];
}
- (NSString *)queryDescription {
if (_queryDescription) {
return _queryDescription;
}
// Grab C++ symbol to describe FIRQuery.query
static ReturnsString firebase_firestore_core_query_tostring = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Is Firebase available?
if (NSClassFromString(@"FIRDocumentReference")) {
firebase_firestore_core_query_tostring = (ReturnsString)dlsym(
RTLD_DEFAULT, "_ZNK8firebase9firestore4core5Query8ToStringEv"
);
}
});
if (!firebase_firestore_core_query_tostring) {
return @"nil";
}
FIRQuery *query = self.initiator_query;
if (!query) return nil;
void *core_query = query.query;
std::string description = firebase_firestore_core_query_tostring(core_query);
// Query strings are like 'Query(canonical_id=...)' so I remove the leading part, and the ()
NSString *prefix = @"Query(canonical_id=";
NSString *desc = @(description.c_str());
desc = [desc stringByReplacingOccurrencesOfString:prefix withString:@""];
desc = [desc stringByReplacingCharactersInRange:NSMakeRange(desc.length-1, 1) withString:@""];
_queryDescription = desc;
return _queryDescription;
}
- (FIRDocumentReference *)initiator_doc {
if ([_initiator isKindOfClass:cFIRDocumentReference]) {
return _initiator;
}
return nil;
}
- (FIRQuery *)initiator_query {
if ([_initiator isKindOfClass:cFIRQuery]) {
return _initiator;
}
return nil;
}
- (FIRCollectionReference *)initiator_collection {
if ([_initiator isKindOfClass:cFIRCollectionReference]) {
return _initiator;
}
return nil;
}
- (FLEXFirebaseSetDataInfo *)setDataInfo {
if (self.requestType == FLEXFIRRequestTypeSetData) {
return self.extraData;
}
return nil;
}
- (NSDictionary *)updateData {
if (self.requestType == FLEXFIRRequestTypeUpdateData) {
return self.extraData;
}
return nil;
}
- (NSString *)path {
switch (self.direction) {
case FLEXFIRTransactionDirectionNone:
return nil;
case FLEXFIRTransactionDirectionPush:
case FLEXFIRTransactionDirectionPull: {
switch (self.requestType) {
case FLEXFIRRequestTypeNotFirebase:
@throw NSInternalInconsistencyException;
case FLEXFIRRequestTypeFetchQuery:
case FLEXFIRRequestTypeAddDocument:
return self.initiator_collection.path ?: self.queryDescription;
case FLEXFIRRequestTypeFetchDocument:
case FLEXFIRRequestTypeSetData:
case FLEXFIRRequestTypeUpdateData:
case FLEXFIRRequestTypeDeleteDocument:
return self.initiator_doc.path;
}
}
}
return nil;
}
- (NSString *)primaryDescription {
if (!_primaryDescription) {
_primaryDescription = self.path.lastPathComponent;
}
return _primaryDescription;
}
- (NSString *)secondaryDescription {
if (!_secondaryDescription) {
_secondaryDescription = self.path.stringByDeletingLastPathComponent;
}
return _secondaryDescription;
}
- (NSString *)tertiaryDescription {
if (!_tertiaryDescription) {
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
NSString *timestamp = [self timestampStringFromRequestDate:self.startTime];
if (timestamp.length > 0) {
[detailComponents addObject:timestamp];
}
[detailComponents addObject:self.direction == FLEXFIRTransactionDirectionPush ?
@"Push ↑" : @"Pull ↓"
];
if (self.direction == FLEXFIRTransactionDirectionPush) {
[detailComponents addObjectsFromArray:@[FLEXStringFromFIRRequestType(self.requestType)]];
}
if (self.state == FLEXNetworkTransactionStateFinished || self.state == FLEXNetworkTransactionStateFailed) {
if (self.direction == FLEXFIRTransactionDirectionPull) {
NSString *docCount = [NSString stringWithFormat:@"%@ document(s)", @(self.documents.count)];
[detailComponents addObjectsFromArray:@[docCount]];
}
} else {
// Unstarted, Awaiting Response, Receiving Data, etc.
NSString *state = [self.class readableStringFromTransactionState:self.state];
[detailComponents addObject:state];
}
_tertiaryDescription = [detailComponents componentsJoinedByString:@" ・ "];
}
return _tertiaryDescription;
}
- (NSString *)copyString {
return self.path;
}
- (BOOL)matchesQuery:(NSString *)filterString {
if ([self.path localizedCaseInsensitiveContainsString:filterString]) {
return YES;
}
BOOL isPull = self.direction == FLEXFIRTransactionDirectionPull;
BOOL isPush = self.direction == FLEXFIRTransactionDirectionPush;
// Allow filtering for push or pull directly
if (isPull && [filterString localizedCaseInsensitiveCompare:@"pull"] == NSOrderedSame) {
return YES;
}
if (isPush && [filterString localizedCaseInsensitiveCompare:@"push"] == NSOrderedSame) {
return YES;
}
return NO;
}
//- (NSString *)responseString {
// if (!_responseString) {
// _responseString = [NSString stringWithUTF8String:(char *)self.response.bytes];
// }
//
// return _responseString;
//}
//
//- (NSDictionary *)responseObject {
// if (!_responseObject) {
// _responseObject = [NSJSONSerialization JSONObjectWithData:self.response options:0 error:nil];
// }
//
// return _responseObject;
//}
@end

View File

@@ -0,0 +1,17 @@
//
// FLEXHTTPTransactionDetailController.h
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXHTTPTransaction;
@interface FLEXHTTPTransactionDetailController : UITableViewController
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction;
@end

View File

@@ -0,0 +1,535 @@
//
// FLEXNetworkTransactionDetailController.m
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXHTTPTransactionDetailController.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"
#import "UIBarButtonItem+FLEX.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 FLEXHTTPTransactionDetailController ()
@property (nonatomic, readonly) FLEXHTTPTransaction *transaction;
@property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
@end
@implementation FLEXHTTPTransactionDetailController
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction {
FLEXHTTPTransactionDetailController *controller = [self new];
controller.transaction = transaction;
return controller;
}
- (instancetype)initWithStyle:(UITableViewStyle)style {
// Force grouped style
return [super initWithStyle:UITableViewStyleGrouped];
}
- (void)viewDidLoad {
[super viewDidLoad];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(handleTransactionUpdatedNotification:)
name:kFLEXNetworkRecorderTransactionUpdatedNotification
object:nil
];
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
[UIBarButtonItem
flex_itemWithTitle:@"Copy curl"
target:self
action:@selector(copyButtonPressed:)
]
];
[self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineCell];
}
- (void)setTransaction:(FLEXHTTPTransaction *)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 new];
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
maxWidth:tableView.bounds.size.width
style:tableView.style
showsAccessory:showsAccessory
];
}
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [NSArray flex_forEachUpTo:self.sections.count map:^id(NSUInteger i) {
return @"⦁";
}];
}
- (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;
}
}
- (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]
];
}
];
}
#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 : UIFont.flex_defaultTableCellFont,
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:(FLEXHTTPTransaction *)transaction {
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
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.
weakify(responseData)
responseBodyRow.selectionFuture = ^UIViewController *() { strongify(responseData)
// Show the response if we can
NSString *contentType = transaction.response.MIMEType;
if (responseData) {
UIViewController *bodyDetails = [self detailViewControllerForMIMEType:contentType data:responseData];
if (bodyDetails) {
bodyDetails.title = @"Response";
return bodyDetails;
}
}
// We can't show the response, alert user
return [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Unable to View Response");
if (responseData) {
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:(FLEXHTTPTransaction *)transaction {
FLEXNetworkDetailSection *requestHeadersSection = [FLEXNetworkDetailSection new];
requestHeadersSection.title = @"Request Headers";
requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
return requestHeadersSection;
}
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXHTTPTransaction *)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"]) {
NSData *body = [self postBodyDataForTransaction:transaction];
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
postBodySection.rows = [self networkDetailRowsFromQueryItems:[FLEXUtility itemsFromQueryString:bodyString]];
}
}
return postBodySection;
}
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXHTTPTransaction *)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:(FLEXHTTPTransaction *)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 forImage: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:(FLEXHTTPTransaction *)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

View File

@@ -0,0 +1,33 @@
//
// FLEXMITMDataSource.h
// FLEX
//
// Created by Tanner Bennett on 8/22/21.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXMITMDataSource<__covariant TransactionType> : NSObject
+ (instancetype)dataSourceWithProvider:(NSArray<TransactionType> *(^)(void))future;
/// Whether or not the data in \c transactions and \c bytesReceived are actually filtered yet or not
@property (nonatomic, readonly) BOOL isFiltered;
/// The content of this array is filtered to match the input of \c filter:completion:
@property (nonatomic, readonly) NSArray<TransactionType> *transactions;
@property (nonatomic, readonly) NSArray<TransactionType> *allTransactions;
/// The content of this array is filtered to match the input of \c filter:completion:
@property (nonatomic) NSInteger bytesReceived;
@property (nonatomic) NSInteger totalBytesReceived;
- (void)reloadByteCounts;
- (void)reloadData:(void (^_Nullable)(FLEXMITMDataSource *dataSource))completion;
- (void)filter:(NSString *)searchString completion:(void(^_Nullable)(FLEXMITMDataSource *dataSource))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,104 @@
//
// FLEXMITMDataSource.m
// FLEX
//
// Created by Tanner Bennett on 8/22/21.
//
#import "FLEXMITMDataSource.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
@interface FLEXMITMDataSource ()
@property (nonatomic, readonly) NSArray *(^dataProvider)(void);
@property (nonatomic) NSString *filterString;
@end
@implementation FLEXMITMDataSource
+ (instancetype)dataSourceWithProvider:(NSArray<id> *(^)(void))future {
FLEXMITMDataSource *ds = [self new];
ds->_dataProvider = future;
[ds reloadData:nil];
return ds;
}
- (BOOL)isFiltered {
return self.filterString.length > 0;
}
- (void)reloadByteCounts {
[self updateBytesReceived];
[self updateFilteredBytesReceived];
}
- (void)reloadData:(void (^)(FLEXMITMDataSource *dataSource))completion {
self.allTransactions = self.dataProvider();
[self filter:self.filterString completion:completion];
}
- (void)filter:(NSString *)searchString completion:(void (^)(FLEXMITMDataSource *dataSource))completion {
self.filterString = searchString;
if (!searchString.length) {
self.filteredTransactions = self.allTransactions;
if (completion) completion(self);
} else {
NSArray<FLEXNetworkTransaction *> *allTransactions = self.allTransactions.copy;
[self onBackgroundQueue:^NSArray *{
return [allTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
return [entry matchesQuery:searchString];
}];
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
if ([self.filterString isEqual:searchString]) {
self.filteredTransactions = filteredNetworkTransactions;
if (completion) completion(self);
}
}];
}
}
- (void)setAllTransactions:(NSArray *)transactions {
_allTransactions = transactions.copy;
[self updateBytesReceived];
}
/// This is really just a semantic setter for \c _transactions
- (void)setFilteredTransactions:(NSArray *)filteredTransactions {
_transactions = filteredTransactions.copy;
[self updateFilteredBytesReceived];
}
- (void)setTransactions:(NSArray *)transactions {
self.filteredTransactions = transactions;
}
- (void)updateBytesReceived {
NSInteger bytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.transactions) {
bytesReceived += transaction.receivedDataLength;
}
self.bytesReceived = bytesReceived;
}
- (void)updateFilteredBytesReceived {
NSInteger filteredBytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.transactions) {
filteredBytesReceived += transaction.receivedDataLength;
}
self.bytesReceived = filteredBytesReceived;
}
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
@end

View File

@@ -0,0 +1,19 @@
//
// FLEXCurlLogger.h
//
//
// Created by Ji Pei on 07/27/16
//
#import <Foundation/Foundation.h>
@interface FLEXNetworkCurlLogger : NSObject
/**
* Generates a cURL command equivalent to the given request.
*
* @param request The request to be translated
*/
+ (NSString *)curlCommandString:(NSURLRequest *)request;
@end

View File

@@ -0,0 +1,38 @@
//
// FLEXCurlLogger.m
//
//
// Created by Ji Pei on 07/27/16
//
#import "FLEXNetworkCurlLogger.h"
@implementation FLEXNetworkCurlLogger
+ (NSString *)curlCommandString:(NSURLRequest *)request {
__block NSMutableString *curlCommandString = [NSMutableString stringWithFormat:@"curl -v -X %@ ", request.HTTPMethod];
[curlCommandString appendFormat:@"\'%@\' ", request.URL.absoluteString];
[request.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *val, BOOL *stop) {
[curlCommandString appendFormat:@"-H \'%@: %@\' ", key, val];
}];
NSArray<NSHTTPCookie *> *cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage cookiesForURL:request.URL];
if (cookies) {
[curlCommandString appendFormat:@"-H \'Cookie:"];
for (NSHTTPCookie *cookie in cookies) {
[curlCommandString appendFormat:@" %@=%@;", cookie.name, cookie.value];
}
[curlCommandString appendFormat:@"\' "];
}
if (request.HTTPBody) {
NSString *body = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
[curlCommandString appendFormat:@"-d \'%@\'", body];
}
return curlCommandString;
}
@end

View File

@@ -0,0 +1,15 @@
//
// FLEXNetworkMITMViewController.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
/// The main screen for the network observer, which displays a list of network transactions.
@interface FLEXNetworkMITMViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end

View File

@@ -0,0 +1,633 @@
//
// FLEXNetworkMITMViewController.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXUtility.h"
#import "FLEXMITMDataSource.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkTransactionCell.h"
#import "FLEXHTTPTransactionDetailController.h"
#import "FLEXNetworkSettingsController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXWebViewController.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXResources.h"
#import "NSUserDefaults+FLEX.h"
#define kFirebaseAvailable NSClassFromString(@"FIRDocumentReference")
#define kWebsocketsAvailable @available(iOS 13.0, *)
typedef NS_ENUM(NSInteger, FLEXNetworkObserverMode) {
FLEXNetworkObserverModeFirebase = 0,
FLEXNetworkObserverModeREST,
FLEXNetworkObserverModeWebsockets,
};
@interface FLEXNetworkMITMViewController ()
@property (nonatomic) BOOL updateInProgress;
@property (nonatomic) BOOL pendingReload;
@property (nonatomic) FLEXNetworkObserverMode mode;
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXNetworkTransaction *> *dataSource;
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXHTTPTransaction *> *HTTPDataSource;
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXWebsocketTransaction *> *websocketDataSource;
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXFirebaseTransaction *> *firebaseDataSource;
@end
@implementation FLEXNetworkMITMViewController
#pragma mark - Lifecycle
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.pinSearchBar = YES;
self.showSearchBarInitially = NO;
NSMutableArray *scopeTitles = [NSMutableArray arrayWithObject:@"REST"];
_HTTPDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
return FLEXNetworkRecorder.defaultRecorder.HTTPTransactions;
}];
if (kFirebaseAvailable) {
_firebaseDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
return FLEXNetworkRecorder.defaultRecorder.firebaseTransactions;
}];
[scopeTitles insertObject:@"Firebase" atIndex:0]; // First space
}
if (kWebsocketsAvailable) {
[scopeTitles addObject:@"Websockets"]; // Last space
_websocketDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
return FLEXNetworkRecorder.defaultRecorder.websocketTransactions;
}];
}
// Scopes will only be shown if we have either firebase or websockets available
self.searchController.searchBar.showsScopeBar = scopeTitles.count > 1;
self.searchController.searchBar.scopeButtonTitles = scopeTitles;
self.mode = NSUserDefaults.standardUserDefaults.flex_lastNetworkObserverMode;
[self addToolbarItems:@[
[UIBarButtonItem
flex_itemWithImage:FLEXResources.gearIcon
target:self
action:@selector(settingsButtonTapped:)
],
[[UIBarButtonItem
flex_systemItem:UIBarButtonSystemItemTrash
target:self
action:@selector(trashButtonTapped:)
] flex_withTintColor:UIColor.redColor]
]];
[self.tableView
registerClass:FLEXNetworkTransactionCell.class
forCellReuseIdentifier:FLEXNetworkTransactionCell.reuseID
];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.rowHeight = FLEXNetworkTransactionCell.preferredCellHeight;
[self registerForNotifications];
[self updateTransactions:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Reload the table if we received updates while not on-screen
if (self.pendingReload) {
[self.tableView reloadData];
self.pendingReload = NO;
}
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (void)registerForNotifications {
NSDictionary *notifications = @{
kFLEXNetworkRecorderNewTransactionNotification:
NSStringFromSelector(@selector(handleNewTransactionRecordedNotification:)),
kFLEXNetworkRecorderTransactionUpdatedNotification:
NSStringFromSelector(@selector(handleTransactionUpdatedNotification:)),
kFLEXNetworkRecorderTransactionsClearedNotification:
NSStringFromSelector(@selector(handleTransactionsClearedNotification:)),
kFLEXNetworkObserverEnabledStateChangedNotification:
NSStringFromSelector(@selector(handleNetworkObserverEnabledStateChangedNotification:)),
};
for (NSString *name in notifications.allKeys) {
[NSNotificationCenter.defaultCenter addObserver:self
selector:NSSelectorFromString(notifications[name]) name:name object:nil
];
}
}
#pragma mark - Private
#pragma mark Button Actions
- (void)settingsButtonTapped:(UIBarButtonItem *)sender {
UIViewController *settings = [FLEXNetworkSettingsController new];
settings.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(
Done, self, @selector(settingsViewControllerDoneTapped:)
);
settings.title = @"Network Debugging Settings";
// This is not a FLEXNavigationController because it is not intended as a new tab
UIViewController *nav = [[UINavigationController alloc] initWithRootViewController:settings];
[self presentViewController:nav animated:YES completion:nil];
}
- (void)trashButtonTapped:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
BOOL clearAll = !self.dataSource.isFiltered;
if (!clearAll) {
make.title(@"Clear Filtered Requests?");
make.message(@"This will only remove the requests matching your search string on this screen.");
} else {
make.title(@"Clear All Recorded Requests?");
make.message(@"This cannot be undone.");
}
make.button(@"Cancel").cancelStyle();
make.button(@"Clear").destructiveStyle().handler(^(NSArray *strings) {
if (clearAll) {
[FLEXNetworkRecorder.defaultRecorder clearRecordedActivity];
} else {
FLEXNetworkTransactionKind kind = (FLEXNetworkTransactionKind)self.mode;
[FLEXNetworkRecorder.defaultRecorder clearRecordedActivity:kind matching:self.searchText];
}
});
} showFrom:self source:sender];
}
- (void)settingsViewControllerDoneTapped:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark Transactions
- (FLEXNetworkObserverMode)mode {
FLEXNetworkObserverMode mode = self.searchController.searchBar.selectedScopeButtonIndex;
switch (mode) {
case FLEXNetworkObserverModeFirebase:
if (kFirebaseAvailable) {
return FLEXNetworkObserverModeFirebase;
}
return FLEXNetworkObserverModeREST;
case FLEXNetworkObserverModeREST:
if (kFirebaseAvailable) {
return FLEXNetworkObserverModeREST;
}
return FLEXNetworkObserverModeWebsockets;
case FLEXNetworkObserverModeWebsockets:
return FLEXNetworkObserverModeWebsockets;
}
}
- (void)setMode:(FLEXNetworkObserverMode)mode {
// The segmentd control will have different appearances based on which APIs
// are available. For example, when only Websockets is available:
//
// 0 1
//
// REST Websockets
//
//
// And when both Firebase and Websockets are available:
//
// 0 1 2
//
// Firebase REST Websockets
//
//
// As a result, we need to adjust the input mode variable accordingly
// before we actually set it. When we try to set it to Firebase but
// Firebase is not available, we don't do anything, because when Firebase
// is unavailable, FLEXNetworkObserverModeFirebase represents the same index
// as REST would without Firebase. For each of the others, we subtract 1
// from them for every relevant API that is unavailable. So for Websockets,
// if it is unavailable, we subtract 1 and it becomes FLEXNetworkObserverModeREST.
// And if Firebase is also unavailable, we subtract 1 again.
switch (mode) {
case FLEXNetworkObserverModeFirebase:
// Will default to REST if Firebase is unavailable
break;
case FLEXNetworkObserverModeREST:
// Firebase will become REST when Firebase is unavailable
if (!kFirebaseAvailable) {
mode--;
}
break;
case FLEXNetworkObserverModeWebsockets:
// Default to REST if Websockets are unavailable
if (!kWebsocketsAvailable) {
mode--;
}
// Firebase will become REST when Firebase is unavailable
if (!kFirebaseAvailable) {
mode--;
}
}
self.searchController.searchBar.selectedScopeButtonIndex = mode;
}
- (FLEXMITMDataSource<FLEXNetworkTransaction *> *)dataSource {
switch (self.mode) {
case FLEXNetworkObserverModeREST:
return self.HTTPDataSource;
case FLEXNetworkObserverModeWebsockets:
return self.websocketDataSource;
case FLEXNetworkObserverModeFirebase:
return self.firebaseDataSource;
}
}
- (void)updateTransactions:(void(^)(void))callback {
id completion = ^(FLEXMITMDataSource *dataSource) {
// Update byte count
[self updateFirstSectionHeader];
if (callback && dataSource == self.dataSource) callback();
};
[self.HTTPDataSource reloadData:completion];
[self.websocketDataSource reloadData:completion];
[self.firebaseDataSource reloadData:completion];
}
#pragma mark Header
- (void)updateFirstSectionHeader {
UIView *view = [self.tableView headerViewForSection:0];
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.text = [self headerText];
[headerView setNeedsLayout];
}
}
- (NSString *)headerText {
long long bytesReceived = self.dataSource.bytesReceived;
NSInteger totalRequests = self.dataSource.transactions.count;
NSString *byteCountText = [NSByteCountFormatter
stringFromByteCount:bytesReceived countStyle:NSByteCountFormatterCountStyleBinary
];
NSString *requestsText = totalRequests == 1 ? @"Request" : @"Requests";
// Exclude byte count from Firebase
if (self.mode == FLEXNetworkObserverModeFirebase) {
return [NSString stringWithFormat:@"%@ %@",
@(totalRequests), requestsText
];
}
return [NSString stringWithFormat:@"%@ %@ (%@ received)",
@(totalRequests), requestsText, byteCountText
];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"📡 Network History";
}
+ (FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
return ^(UITableViewController *host) {
if (FLEXNetworkObserver.isEnabled) {
[host.navigationController pushViewController:[
self globalsEntryViewController:row
] animated:YES];
} else {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Network Monitor Disabled");
make.message(@"You must enable network monitoring to proceed.");
make.button(@"Turn On").handler(^(NSArray<NSString *> *strings) {
FLEXNetworkObserver.enabled = YES;
[host.navigationController pushViewController:[
self globalsEntryViewController:row
] animated:YES];
}).cancelStyle();
make.button(@"Dismiss");
} showFrom:host];
}
};
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
UIViewController *controller = [self new];
controller.title = [self globalsEntryTitle:row];
return controller;
}
#pragma mark - Notification Handlers
- (void)handleNewTransactionRecordedNotification:(NSNotification *)notification {
[self tryUpdateTransactions];
}
- (void)tryUpdateTransactions {
// Don't do any view updating if we aren't in the view hierarchy
if (!self.viewIfLoaded.window) {
[self updateTransactions:nil];
self.pendingReload = YES;
return;
}
// Let the previous row insert animation finish before starting a new one to avoid stomping.
// We'll try calling the method again when the insertion completes,
// and we properly no-op if there haven't been changes.
if (self.updateInProgress) {
return;
}
self.updateInProgress = YES;
// Get state before update
NSString *currentFilter = self.searchText;
FLEXNetworkObserverMode currentMode = self.mode;
NSInteger existingRowCount = self.dataSource.transactions.count;
[self updateTransactions:^{
// Compare to state after update
NSString *newFilter = self.searchText;
FLEXNetworkObserverMode newMode = self.mode;
NSInteger newRowCount = self.dataSource.transactions.count;
NSInteger rowCountDiff = newRowCount - existingRowCount;
// Abort if the observation mode changed, or if the search field text changed
if (newMode != currentMode || ![currentFilter isEqualToString:newFilter]) {
self.updateInProgress = NO;
return;
}
if (rowCountDiff) {
// Insert animation if we're at the top.
if (self.tableView.contentOffset.y <= 0.0 && rowCountDiff > 0) {
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.updateInProgress = NO;
// This isn't an infinite loop, it won't run a third time
// if there were no new transactions the second time
[self tryUpdateTransactions];
}];
NSMutableArray<NSIndexPath *> *indexPathsToReload = [NSMutableArray new];
for (NSInteger row = 0; row < rowCountDiff; row++) {
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
}
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[CATransaction commit];
} else {
// Maintain the user's position if they've scrolled down.
CGSize existingContentSize = self.tableView.contentSize;
[self.tableView reloadData];
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
self.updateInProgress = NO;
}
} else {
self.updateInProgress = NO;
}
}];
}
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification {
[self.HTTPDataSource reloadByteCounts];
[self.websocketDataSource reloadByteCounts];
// Don't need to reload Firebase here
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
// Update both the main table view and search table view if needed.
for (FLEXNetworkTransactionCell *cell in self.tableView.visibleCells) {
if ([cell.transaction isEqual:transaction]) {
// Using -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] is overkill here and kicks off a lot of
// work that can make the table view somewhat unresponsive when lots of updates are streaming in.
// We just need to tell the cell that it needs to re-layout.
[cell setNeedsLayout];
break;
}
}
[self updateFirstSectionHeader];
}
- (void)handleTransactionsClearedNotification:(NSNotification *)notification {
[self updateTransactions:^{
[self.tableView reloadData];
}];
}
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification {
// Update the header, which displays a warning when network debugging is disabled
[self updateFirstSectionHeader];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataSource.transactions.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [self headerText];
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.font = [UIFont systemFontOfSize:14.0 weight:UIFontWeightSemibold];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXNetworkTransactionCell *cell = [tableView
dequeueReusableCellWithIdentifier:FLEXNetworkTransactionCell.reuseID
forIndexPath:indexPath
];
cell.transaction = [self transactionAtIndexPath:indexPath];
// Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
NSInteger totalRows = [tableView numberOfRowsInSection:indexPath.section];
if ((totalRows - indexPath.row) % 2 == 0) {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
switch (self.mode) {
case FLEXNetworkObserverModeREST: {
FLEXHTTPTransaction *transaction = [self HTTPTransactionAtIndexPath:indexPath];
UIViewController *details = [FLEXHTTPTransactionDetailController withTransaction:transaction];
[self.navigationController pushViewController:details animated:YES];
break;
}
case FLEXNetworkObserverModeWebsockets: {
if (@available(iOS 13.0, *)) { // This check will never fail
FLEXWebsocketTransaction *transaction = [self websocketTransactionAtIndexPath:indexPath];
UIViewController *details = nil;
if (transaction.message.type == NSURLSessionWebSocketMessageTypeData) {
details = [FLEXObjectExplorerFactory explorerViewControllerForObject:transaction.message.data];
} else {
details = [[FLEXWebViewController alloc] initWithText:transaction.message.string];
}
[self.navigationController pushViewController:details animated:YES];
}
break;
}
case FLEXNetworkObserverModeFirebase: {
FLEXFirebaseTransaction *transaction = [self firebaseTransactionAtIndexPath:indexPath];
// id obj = transaction.documents.count == 1 ? transaction.documents.firstObject : transaction.documents;
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:transaction];
[self.navigationController pushViewController:explorer animated:YES];
}
}
}
#pragma mark - Menu Actions
- (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:)) {
UIPasteboard.generalPasteboard.string = [self transactionAtIndexPath:indexPath].copyString;
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
FLEXNetworkTransaction *transaction = [self transactionAtIndexPath:indexPath];
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction
actionWithTitle:@"Copy URL"
image:nil
identifier:nil
handler:^(__kindof UIAction *action) {
UIPasteboard.generalPasteboard.string = transaction.copyString;
}
];
NSArray *children = @[copy];
if (self.mode == FLEXNetworkObserverModeREST) {
NSURLRequest *request = [self HTTPTransactionAtIndexPath:indexPath].request;
UIAction *denylist = [UIAction
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
image:nil
identifier:nil
handler:^(__kindof UIAction *action) {
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
[denylist addObject:request.URL.host];
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
[self tryUpdateTransactions];
}
];
children = [children arrayByAddingObject:denylist];
}
return [UIMenu
menuWithTitle:@"" image:nil identifier:nil
options:UIMenuOptionsDisplayInline
children:children
];
}
];
}
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath {
return self.dataSource.transactions[indexPath.row];
}
- (FLEXHTTPTransaction *)HTTPTransactionAtIndexPath:(NSIndexPath *)indexPath {
return self.HTTPDataSource.transactions[indexPath.row];
}
- (FLEXWebsocketTransaction *)websocketTransactionAtIndexPath:(NSIndexPath *)indexPath {
return self.websocketDataSource.transactions[indexPath.row];
}
- (FLEXFirebaseTransaction *)firebaseTransactionAtIndexPath:(NSIndexPath *)indexPath {
return self.firebaseDataSource.transactions[indexPath.row];
}
#pragma mark - Search Bar
- (void)updateSearchResults:(NSString *)searchString {
id callback = ^(FLEXMITMDataSource *dataSource) {
if (self.dataSource == dataSource) {
[self.tableView reloadData];
}
};
[self.HTTPDataSource filter:searchString completion:callback];
[self.websocketDataSource filter:searchString completion:callback];
[self.firebaseDataSource filter:searchString completion:callback];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)newScope {
[self updateFirstSectionHeader];
[self.tableView reloadData];
NSUserDefaults.standardUserDefaults.flex_lastNetworkObserverMode = self.mode;
}
- (void)willDismissSearchController:(UISearchController *)searchController {
[self.tableView reloadData];
}
@end

View File

@@ -0,0 +1,122 @@
//
// FLEXNetworkRecorder.h
// Flipboard
//
// Created by Ryan Olson on 2/4/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
// Notifications posted when the record is updated
extern NSString *const kFLEXNetworkRecorderNewTransactionNotification;
extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction, FLEXFirebaseTransaction;
@class FIRQuery, FIRDocumentReference, FIRCollectionReference, FIRDocumentSnapshot, FIRQuerySnapshot;
typedef NS_ENUM(NSUInteger, FLEXNetworkTransactionKind) {
FLEXNetworkTransactionKindFirebase = 0,
FLEXNetworkTransactionKindREST,
FLEXNetworkTransactionKindWebsockets,
};
@interface FLEXNetworkRecorder : NSObject
/// In general, it only makes sense to have one recorder for the entire application.
@property (nonatomic, readonly, class) FLEXNetworkRecorder *defaultRecorder;
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
@property (nonatomic) NSUInteger responseCacheByteLimit;
/// If NO, the recorder not cache will not cache response for content types
/// with an "image", "video", or "audio" prefix.
@property (nonatomic) BOOL shouldCacheMediaResponses;
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
/// Call this after adding to or setting the \c hostDenylist to remove excluded transactions
- (void)clearExcludedTransactions;
/// Call this to save the denylist to the disk to be loaded next time
- (void)synchronizeDenylist;
#pragma mark Accessing recorded network activity
/// Array of FLEXHTTPTransaction objects ordered by start time with the newest first.
@property (nonatomic, readonly) NSArray<FLEXHTTPTransaction *> *HTTPTransactions;
/// Array of FLEXWebsocketTransaction objects ordered by start time with the newest first.
@property (nonatomic, readonly) NSArray<FLEXWebsocketTransaction *> *websocketTransactions API_AVAILABLE(ios(13.0));
/// Array of FLEXFirebaseTransaction objects ordered by start time with the newest first.
@property (nonatomic, readonly) NSArray<FLEXFirebaseTransaction *> *firebaseTransactions;
/// The full response data IFF it hasn't been purged due to memory pressure.
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction;
/// Dumps all network transactions and cached response bodies.
- (void)clearRecordedActivity;
/// Clear only transactions matching the given query.
- (void)clearRecordedActivity:(FLEXNetworkTransactionKind)kind matching:(NSString *)query;
#pragma mark Recording network activity
/// Call when app is about to send HTTP request.
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
request:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse;
/// Call when HTTP response is available.
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response;
/// Call when data chunk is received over the network.
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength;
/// Call when HTTP request has finished loading.
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody;
/// Call when HTTP request has failed to load.
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error;
/// Call to set the request mechanism anytime after recordRequestWillBeSent... has been called.
/// This string can be set to anything useful about the API used to make the request.
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
error:(NSError *)error API_AVAILABLE(ios(13.0));
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
- (void)recordFIRQueryWillFetch:(FIRQuery *)query withTransactionID:(NSString *)transactionID;
- (void)recordFIRDocumentWillFetch:(FIRDocumentReference *)document withTransactionID:(NSString *)transactionID;
- (void)recordFIRQueryDidFetch:(FIRQuerySnapshot *)response error:(NSError *)error
transactionID:(NSString *)transactionID;
- (void)recordFIRDocumentDidFetch:(FIRDocumentSnapshot *)response error:(NSError *)error
transactionID:(NSString *)transactionID;
- (void)recordFIRWillSetData:(FIRDocumentReference *)doc
data:(NSDictionary *)documentData
merge:(NSNumber *)yesorno
mergeFields:(NSArray *)fields
transactionID:(NSString *)transactionID;
- (void)recordFIRWillUpdateData:(FIRDocumentReference *)doc fields:(NSDictionary *)fields
transactionID:(NSString *)transactionID;
- (void)recordFIRWillDeleteDocument:(FIRDocumentReference *)doc transactionID:(NSString *)transactionID;
- (void)recordFIRWillAddDocument:(FIRCollectionReference *)initiator
document:(FIRDocumentReference *)doc
transactionID:(NSString *)transactionID;
- (void)recordFIRDidSetData:(NSError *)error transactionID:(NSString *)transactionID;
- (void)recordFIRDidUpdateData:(NSError *)error transactionID:(NSString *)transactionID;
- (void)recordFIRDidDeleteDocument:(NSError *)error transactionID:(NSString *)transactionID;
- (void)recordFIRDidAddDocument:(NSError *)error transactionID:(NSString *)transactionID;
@end

View File

@@ -0,0 +1,500 @@
//
// FLEXNetworkRecorder.m
// Flipboard
//
// Created by Ryan Olson on 2/4/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkCurlLogger.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
#import "NSUserDefaults+FLEX.h"
#import "OSCache.h"
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
@interface FLEXNetworkRecorder ()
@property (nonatomic) OSCache *restCache;
@property (atomic) NSMutableArray<FLEXHTTPTransaction *> *orderedHTTPTransactions;
@property (atomic) NSMutableArray<FLEXWebsocketTransaction *> *orderedWSTransactions;
@property (atomic) NSMutableArray<FLEXFirebaseTransaction *> *orderedFirebaseTransactions;
@property (atomic) NSMutableDictionary<NSString *, __kindof FLEXNetworkTransaction *> *requestIDsToTransactions;
@property (nonatomic) dispatch_queue_t queue;
@end
@implementation FLEXNetworkRecorder
- (instancetype)init {
self = [super init];
if (self) {
self.restCache = [OSCache new];
NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults
objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue
];
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
self.restCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
[self.restCache setTotalCostLimit:responseCacheLimit];
self.orderedWSTransactions = [NSMutableArray new];
self.orderedHTTPTransactions = [NSMutableArray new];
self.orderedFirebaseTransactions = [NSMutableArray new];
self.requestIDsToTransactions = [NSMutableDictionary new];
self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.mutableCopy;
// Serial queue used because we use mutable objects that are not thread safe
self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
}
return self;
}
+ (instancetype)defaultRecorder {
static FLEXNetworkRecorder *defaultRecorder = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultRecorder = [self new];
});
return defaultRecorder;
}
#pragma mark - Public Data Access
- (NSUInteger)responseCacheByteLimit {
return self.restCache.totalCostLimit;
}
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit {
self.restCache.totalCostLimit = responseCacheByteLimit;
[NSUserDefaults.standardUserDefaults
setObject:@(responseCacheByteLimit)
forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey
];
}
- (NSArray<FLEXHTTPTransaction *> *)HTTPTransactions {
return self.orderedHTTPTransactions.copy;
}
- (NSArray<FLEXWebsocketTransaction *> *)websocketTransactions {
return self.orderedWSTransactions.copy;
}
- (NSArray<FLEXFirebaseTransaction *> *)firebaseTransactions {
return self.orderedFirebaseTransactions.copy;
}
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction {
return [self.restCache objectForKey:transaction.requestID];
}
- (void)clearRecordedActivity {
dispatch_async(self.queue, ^{
[self.restCache removeAllObjects];
[self.orderedWSTransactions removeAllObjects];
[self.orderedHTTPTransactions removeAllObjects];
[self.orderedFirebaseTransactions removeAllObjects];
[self.requestIDsToTransactions removeAllObjects];
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
});
}
- (void)clearRecordedActivity:(FLEXNetworkTransactionKind)kind matching:(NSString *)query {
dispatch_async(self.queue, ^{
switch (kind) {
case FLEXNetworkTransactionKindFirebase: {
[self.orderedFirebaseTransactions flex_filter:^BOOL(FLEXFirebaseTransaction *obj, NSUInteger idx) {
return ![obj matchesQuery:query];
}];
break;
}
case FLEXNetworkTransactionKindREST: {
NSArray<FLEXHTTPTransaction *> *toRemove;
toRemove = [self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *obj, NSUInteger idx) {
return [obj matchesQuery:query];
}];
// Remove from cache
for (FLEXHTTPTransaction *t in toRemove) {
[self.restCache removeObjectForKey:t.requestID];
}
// Remove from list
[self.orderedHTTPTransactions removeObjectsInArray:toRemove];
break;
}
case FLEXNetworkTransactionKindWebsockets: {
[self.orderedWSTransactions flex_filter:^BOOL(FLEXWebsocketTransaction *obj, NSUInteger idx) {
return ![obj matchesQuery:query];
}];
break;
}
}
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
});
}
- (void)clearExcludedTransactions {
dispatch_sync(self.queue, ^{
self.orderedHTTPTransactions = ({
[self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *ta, NSUInteger idx) {
NSString *host = ta.request.URL.host;
for (NSString *excluded in self.hostDenylist) {
if ([host hasSuffix:excluded]) {
return NO;
}
}
return YES;
}];
});
});
}
- (void)synchronizeDenylist {
NSUserDefaults.standardUserDefaults.flex_networkHostDenylist = self.hostDenylist;
}
#pragma mark - Network Events
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
request:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse {
for (NSString *host in self.hostDenylist) {
if ([request.URL.host hasSuffix:host]) {
return;
}
}
FLEXHTTPTransaction *transaction = [FLEXHTTPTransaction request:request identifier:requestID];
// Before async block to keep times accurate
if (redirectResponse) {
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
}
// A redirect is always a new request
dispatch_async(self.queue, ^{
[self.orderedHTTPTransactions insertObject:transaction atIndex:0];
self.requestIDsToTransactions[requestID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response {
// Before async block to stay accurate
NSDate *responseDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.response = response;
transaction.state = FLEXNetworkTransactionStateReceivingData;
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.receivedDataLength += dataLength;
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody {
NSDate *finishedDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.state = FLEXNetworkTransactionStateFinished;
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
BOOL shouldCache = responseBody.length > 0;
if (!self.shouldCacheMediaResponses) {
NSArray<NSString *> *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
}
}
if (shouldCache) {
[self.restCache setObject:responseBody forKey:requestID cost:responseBody.length];
}
NSString *mimeType = transaction.response.MIMEType;
if ([mimeType hasPrefix:@"image/"] && responseBody.length > 0) {
// Thumbnail image previews on a separate background queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0;
transaction.thumbnail = [FLEXUtility
thumbnailedImageWithMaxPixelDimension:maxPixelDimension
fromImageData:responseBody
];
[self postUpdateNotificationForTransaction:transaction];
});
} else if ([mimeType isEqual:@"application/json"]) {
transaction.thumbnail = FLEXResources.jsonIcon;
} else if ([mimeType isEqual:@"text/plain"]){
transaction.thumbnail = FLEXResources.textPlainIcon;
} else if ([mimeType isEqual:@"text/html"]) {
transaction.thumbnail = FLEXResources.htmlIcon;
} else if ([mimeType isEqual:@"application/x-plist"]) {
transaction.thumbnail = FLEXResources.plistIcon;
} else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
transaction.thumbnail = FLEXResources.binaryIcon;
} else if ([mimeType containsString:@"javascript"]) {
transaction.thumbnail = FLEXResources.jsIcon;
} else if ([mimeType containsString:@"xml"]) {
transaction.thumbnail = FLEXResources.xmlIcon;
} else if ([mimeType hasPrefix:@"audio"]) {
transaction.thumbnail = FLEXResources.audioIcon;
} else if ([mimeType hasPrefix:@"video"]) {
transaction.thumbnail = FLEXResources.videoIcon;
} else if ([mimeType hasPrefix:@"text"]) {
transaction.thumbnail = FLEXResources.textIcon;
}
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.state = FLEXNetworkTransactionStateFailed;
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
transaction.error = error;
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.requestMechanism = mechanism;
[self postUpdateNotificationForTransaction:transaction];
});
}
#pragma mark - Websocket Events
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
dispatch_async(self.queue, ^{
FLEXWebsocketTransaction *send = [FLEXWebsocketTransaction
withMessage:message task:task direction:FLEXWebsocketOutgoing
];
[self.orderedWSTransactions insertObject:send atIndex:0];
[self postNewTransactionNotificationWithTransaction:send];
});
}
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error {
dispatch_async(self.queue, ^{
FLEXWebsocketTransaction *send = [self.orderedWSTransactions flex_firstWhere:^BOOL(FLEXWebsocketTransaction *t) {
return t.message == message;
}];
send.error = error;
send.state = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
[self postUpdateNotificationForTransaction:send];
});
}
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
dispatch_async(self.queue, ^{
FLEXWebsocketTransaction *receive = [FLEXWebsocketTransaction
withMessage:message task:task direction:FLEXWebsocketIncoming
];
[self.orderedWSTransactions insertObject:receive atIndex:0];
[self postNewTransactionNotificationWithTransaction:receive];
});
}
#pragma mark - Firebase, Reading
- (void)recordFIRQueryWillFetch:(FIRQuery *)query withTransactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction queryFetch:query];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRDocumentWillFetch:(FIRDocumentReference *)document withTransactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction documentFetch:document];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRQueryDidFetch:(FIRQuerySnapshot *)response error:(NSError *)error transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
if (!transaction) {
return;
}
transaction.error = error;
transaction.documents = response.documents;
transaction.state = FLEXNetworkTransactionStateFinished;
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordFIRDocumentDidFetch:(FIRDocumentSnapshot *)response error:(NSError *)error transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
if (!transaction) {
return;
}
transaction.error = error;
transaction.documents = response ? @[response] : @[];
transaction.state = FLEXNetworkTransactionStateFinished;
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
[self postUpdateNotificationForTransaction:transaction];
});
}
#pragma mark Firebase, Writing
- (void)recordFIRWillSetData:(FIRDocumentReference *)doc
data:(NSDictionary *)documentData
merge:(NSNumber *)yesorno
mergeFields:(NSArray *)fields
transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction
setData:doc data:documentData merge:yesorno mergeFields:fields
];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRWillUpdateData:(FIRDocumentReference *)doc fields:(NSDictionary *)fields
transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction updateData:doc data:fields];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRWillDeleteDocument:(FIRDocumentReference *)doc transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction deleteDocument:doc];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRWillAddDocument:(FIRCollectionReference *)initiator document:(FIRDocumentReference *)doc
transactionID:(NSString *)transactionID {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = [FLEXFirebaseTransaction
addDocument:initiator document:doc
];
self.requestIDsToTransactions[transactionID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordFIRDidSetData:(NSError *)error transactionID:(NSString *)transactionID {
[self firebaseTransaction:transactionID didUpdate:error];
}
- (void)recordFIRDidUpdateData:(NSError *)error transactionID:(NSString *)transactionID {
[self firebaseTransaction:transactionID didUpdate:error];
}
- (void)recordFIRDidDeleteDocument:(NSError *)error transactionID:(NSString *)transactionID {
[self firebaseTransaction:transactionID didUpdate:error];
}
- (void)recordFIRDidAddDocument:(NSError *)error transactionID:(NSString *)transactionID {
[self firebaseTransaction:transactionID didUpdate:error];
}
- (void)firebaseTransaction:(NSString *)transactionID didUpdate:(NSError *)error {
dispatch_async(self.queue, ^{
FLEXFirebaseTransaction *transaction = self.requestIDsToTransactions[transactionID];
if (!transaction) {
return;
}
transaction.error = error;
transaction.state = FLEXNetworkTransactionStateFinished;
[self.orderedFirebaseTransactions insertObject:transaction atIndex:0];
[self postUpdateNotificationForTransaction:transaction];
});
}
#pragma mark - Notification Posting
- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction {
[self notify:kFLEXNetworkRecorderNewTransactionNotification transaction:transaction];
}
- (void)postUpdateNotificationForTransaction:(FLEXNetworkTransaction *)transaction {
[self notify:kFLEXNetworkRecorderTransactionUpdatedNotification transaction:transaction];
}
- (void)notify:(NSString *)name transaction:(FLEXNetworkTransaction *)transaction {
NSDictionary *userInfo = nil;
if (transaction) {
userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
}
dispatch_async(dispatch_get_main_queue(), ^{
[NSNotificationCenter.defaultCenter postNotificationName:name object:self userInfo:userInfo];
});
}
@end

View File

@@ -0,0 +1,12 @@
//
// FLEXNetworkSettingsController.h
// FLEXInjected
//
// Created by Ryan Olson on 2/20/15.
//
#import "FLEXTableViewController.h"
@interface FLEXNetworkSettingsController : FLEXTableViewController
@end

View File

@@ -0,0 +1,253 @@
//
// FLEXNetworkSettingsController.m
// FLEXInjected
//
// Created by Ryan Olson on 2/20/15.
//
#import "FLEXNetworkSettingsController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXUtility.h"
#import "FLEXTableView.h"
#import "FLEXColor.h"
#import "NSUserDefaults+FLEX.h"
@interface FLEXNetworkSettingsController () <UIActionSheetDelegate>
@property (nonatomic) float cacheLimitValue;
@property (nonatomic, readonly) NSString *cacheLimitCellTitle;
@property (nonatomic, readonly) UISwitch *observerSwitch;
@property (nonatomic, readonly) UISwitch *cacheMediaSwitch;
@property (nonatomic, readonly) UISwitch *jsonViewerSwitch;
@property (nonatomic, readonly) UISlider *cacheLimitSlider;
@property (nonatomic) UILabel *cacheLimitLabel;
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
@end
@implementation FLEXNetworkSettingsController
- (void)viewDidLoad {
[super viewDidLoad];
[self disableToolbar];
self.hostDenylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist.mutableCopy;
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
_observerSwitch = [UISwitch new];
_cacheMediaSwitch = [UISwitch new];
_jsonViewerSwitch = [UISwitch new];
_cacheLimitSlider = [UISlider new];
self.observerSwitch.on = FLEXNetworkObserver.enabled;
[self.observerSwitch addTarget:self
action:@selector(networkDebuggingToggled:)
forControlEvents:UIControlEventValueChanged
];
self.cacheMediaSwitch.on = FLEXNetworkRecorder.defaultRecorder.shouldCacheMediaResponses;
[self.cacheMediaSwitch addTarget:self
action:@selector(cacheMediaResponsesToggled:)
forControlEvents:UIControlEventValueChanged
];
self.jsonViewerSwitch.on = defaults.flex_registerDictionaryJSONViewerOnLaunch;
[self.jsonViewerSwitch addTarget:self
action:@selector(jsonViewerSettingToggled:)
forControlEvents:UIControlEventValueChanged
];
[self.cacheLimitSlider addTarget:self
action:@selector(cacheLimitAdjusted:)
forControlEvents:UIControlEventValueChanged
];
UISlider *slider = self.cacheLimitSlider;
self.cacheLimitValue = FLEXNetworkRecorder.defaultRecorder.responseCacheByteLimit;
const NSUInteger fiftyMega = 50 * 1024 * 1024;
slider.minimumValue = 0;
slider.maximumValue = fiftyMega;
slider.value = self.cacheLimitValue;
}
- (void)setCacheLimitValue:(float)cacheLimitValue {
_cacheLimitValue = cacheLimitValue;
self.cacheLimitLabel.text = self.cacheLimitCellTitle;
[FLEXNetworkRecorder.defaultRecorder setResponseCacheByteLimit:cacheLimitValue];
}
- (NSString *)cacheLimitCellTitle {
NSInteger cacheLimit = self.cacheLimitValue;
NSInteger limitInMB = round(cacheLimit / (1024 * 1024));
return [NSString stringWithFormat:@"Cache Limit (%@ MB)", @(limitInMB)];
}
#pragma mark - Settings Actions
- (void)networkDebuggingToggled:(UISwitch *)sender {
FLEXNetworkObserver.enabled = sender.isOn;
}
- (void)cacheMediaResponsesToggled:(UISwitch *)sender {
FLEXNetworkRecorder.defaultRecorder.shouldCacheMediaResponses = sender.isOn;
}
- (void)jsonViewerSettingToggled:(UISwitch *)sender {
[NSUserDefaults.standardUserDefaults flex_toggleBoolForKey:kFLEXDefaultsRegisterJSONExplorerKey];
}
- (void)cacheLimitAdjusted:(UISlider *)sender {
self.cacheLimitValue = sender.value;
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.hostDenylist.count ? 2 : 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
switch (section) {
case 0: return 5;
case 1: return self.hostDenylist.count;
default: return 0;
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0: return @"General";
case 1: return @"Host Denylist";
default: return nil;
}
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
if (section == 0) {
return @"By default, JSON is rendered in a webview. Turn on "
"\"View JSON as a dictionary/array\" to convert JSON payloads "
"to objects and view them in an object explorer. "
"This setting requires a restart of the app.";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView
dequeueReusableCellWithIdentifier:kFLEXDefaultCell forIndexPath:indexPath
];
cell.accessoryView = nil;
cell.textLabel.textColor = FLEXColor.primaryTextColor;
switch (indexPath.section) {
// Settings
case 0: {
switch (indexPath.row) {
case 0:
cell.textLabel.text = @"Network Debugging";
cell.accessoryView = self.observerSwitch;
break;
case 1:
cell.textLabel.text = @"Cache Media Responses";
cell.accessoryView = self.cacheMediaSwitch;
break;
case 2:
cell.textLabel.text = @"View JSON as a dictionary/array";
cell.accessoryView = self.jsonViewerSwitch;
break;
case 3:
cell.textLabel.text = @"Reset Host Denylist";
cell.textLabel.textColor = tableView.tintColor;
break;
case 4:
cell.textLabel.text = self.cacheLimitCellTitle;
self.cacheLimitLabel = cell.textLabel;
[self.cacheLimitSlider removeFromSuperview];
[cell.contentView addSubview:self.cacheLimitSlider];
CGRect container = cell.contentView.frame;
UISlider *slider = self.cacheLimitSlider;
[slider sizeToFit];
CGFloat sliderWidth = 150.f;
CGFloat sliderOriginY = FLEXFloor((container.size.height - slider.frame.size.height) / 2.0);
CGFloat sliderOriginX = CGRectGetMaxX(container) - sliderWidth - tableView.separatorInset.left;
self.cacheLimitSlider.frame = CGRectMake(
sliderOriginX, sliderOriginY, sliderWidth, slider.frame.size.height
);
// Make wider, keep in middle of cell, keep to trailing edge of cell
self.cacheLimitSlider.autoresizingMask = ({
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin;
});
break;
}
break;
}
// Denylist entries
case 1: {
cell.textLabel.text = self.hostDenylist[indexPath.row];
break;
}
default:
@throw NSInternalInconsistencyException;
break;
}
return cell;
}
#pragma mark - Table View Delegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)ip {
// Can only select the "Reset Host Denylist" row
return ip.section == 0 && ip.row == 2;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Reset Host Denylist");
make.message(@"You cannot undo this action. Are you sure?");
make.button(@"Reset").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
self.hostDenylist = nil;
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeAllObjects];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
[self.tableView deleteSections:
[NSIndexSet indexSetWithIndex:1]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return indexPath.section == 1;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)style
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(style == UITableViewCellEditingStyleDelete);
NSString *host = self.hostDenylist[indexPath.row];
[self.hostDenylist removeObjectAtIndex:indexPath.row];
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeObject:host];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
@end

View File

@@ -0,0 +1,178 @@
//
// FLEXNetworkTransaction.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "Firestore.h"
typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
FLEXNetworkTransactionStateUnstarted = -1,
/// This is the default; it's usually nonsense for a request to be marked as "unstarted"
FLEXNetworkTransactionStateAwaitingResponse = 0,
FLEXNetworkTransactionStateReceivingData,
FLEXNetworkTransactionStateFinished,
FLEXNetworkTransactionStateFailed
};
typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
FLEXWebsocketIncoming = 1,
FLEXWebsocketOutgoing,
};
/// The shared base class for all types of network transactions.
/// Subclasses should implement the descriptions and details properties, and assign a thumbnail.
@interface FLEXNetworkTransaction : NSObject {
@protected
NSString *_primaryDescription;
NSString *_secondaryDescription;
NSString *_tertiaryDescription;
}
+ (instancetype)withStartTime:(NSDate *)startTime;
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
@property (nonatomic) NSError *error;
/// Subclasses can override to provide error state based on response data as well
@property (nonatomic, readonly) BOOL displayAsError;
@property (nonatomic, readonly) NSDate *startTime;
@property (nonatomic) FLEXNetworkTransactionState state;
@property (nonatomic) int64_t receivedDataLength;
/// A small thumbnail to preview the type of/the response
@property (nonatomic) UIImage *thumbnail;
/// The most prominent line of the cell. Typically a URL endpoint or other distinguishing attribute.
/// This line turns red when the transaction indicates an error.
@property (nonatomic, readonly) NSString *primaryDescription;
/// Something less important, such as a blob of data or the URL's domain.
@property (nonatomic, readonly) NSString *secondaryDescription;
/// Minor details to display at the bottom of the cell, such as a timestamp, HTTP method, or status.
@property (nonatomic, readonly) NSString *tertiaryDescription;
/// The string to copy when the user selects the "copy" action
@property (nonatomic, readonly) NSString *copyString;
/// Whether or not this request should show up when the user searches for a given string
- (BOOL)matchesQuery:(NSString *)filterString;
/// For internal use
- (NSString *)timestampStringFromRequestDate:(NSDate *)date;
@end
/// The shared base class for all NSURL-API-related transactions.
/// Descriptions are generated by this class using the URL provided by subclasses.
@interface FLEXURLTransaction : FLEXNetworkTransaction
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime;
@property (nonatomic, readonly) NSURLRequest *request;
/// Subclasses should implement for when the transaction is complete
@property (nonatomic, readonly) NSArray<NSString *> *details;
@end
@interface FLEXHTTPTransaction : FLEXURLTransaction
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID;
@property (nonatomic, readonly) NSString *requestID;
@property (nonatomic) NSURLResponse *response;
@property (nonatomic, copy) NSString *requestMechanism;
@property (nonatomic) NSTimeInterval latency;
@property (nonatomic) NSTimeInterval duration;
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
@property (nonatomic, readonly) NSData *cachedRequestBody;
@end
@interface FLEXWebsocketTransaction : FLEXURLTransaction
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task
direction:(FLEXWebsocketMessageDirection)direction API_AVAILABLE(ios(13.0));
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task
direction:(FLEXWebsocketMessageDirection)direction
startTime:(NSDate *)started API_AVAILABLE(ios(13.0));
//@property (nonatomic, readonly) NSURLSessionWebSocketTask *task;
@property (nonatomic, readonly) NSURLSessionWebSocketMessage *message API_AVAILABLE(ios(13.0));
@property (nonatomic, readonly) FLEXWebsocketMessageDirection direction API_AVAILABLE(ios(13.0));
@property (nonatomic, readonly) int64_t dataLength API_AVAILABLE(ios(13.0));
@end
typedef NS_ENUM(NSUInteger, FLEXFIRTransactionDirection) {
FLEXFIRTransactionDirectionNone,
FLEXFIRTransactionDirectionPush,
FLEXFIRTransactionDirectionPull,
};
typedef NS_ENUM(NSUInteger, FLEXFIRRequestType) {
FLEXFIRRequestTypeNotFirebase,
FLEXFIRRequestTypeFetchQuery,
FLEXFIRRequestTypeFetchDocument,
FLEXFIRRequestTypeSetData,
FLEXFIRRequestTypeUpdateData,
FLEXFIRRequestTypeAddDocument,
FLEXFIRRequestTypeDeleteDocument,
};
@interface FLEXFirebaseSetDataInfo : NSObject
/// The data that was set
@property (nonatomic, readonly) NSDictionary *documentData;
/// \c nil if \c mergeFields is populated
@property (nonatomic, readonly) NSNumber *merge;
/// \c nil if \c merge is populated
@property (nonatomic, readonly) NSArray *mergeFields;
@end
@interface FLEXFirebaseTransaction : FLEXNetworkTransaction
+ (instancetype)queryFetch:(FIRQuery *)initiator;
+ (instancetype)documentFetch:(FIRDocumentReference *)initiator;
+ (instancetype)setData:(FIRDocumentReference *)initiator
data:(NSDictionary *)data
merge:(NSNumber *)merge
mergeFields:(NSArray *)mergeFields;
+ (instancetype)updateData:(FIRDocumentReference *)initiator data:(NSDictionary *)data;
+ (instancetype)addDocument:(FIRCollectionReference *)initiator document:(FIRDocumentReference *)doc;
+ (instancetype)deleteDocument:(FIRDocumentReference *)initiator;
@property (nonatomic, readonly) FLEXFIRTransactionDirection direction;
@property (nonatomic, readonly) FLEXFIRRequestType requestType;
@property (nonatomic, readonly) id initiator;
@property (nonatomic, readonly) FIRQuery *initiator_query;
@property (nonatomic, readonly) FIRDocumentReference *initiator_doc;
@property (nonatomic, readonly) FIRCollectionReference *initiator_collection;
/// Only used for fetch types
@property (nonatomic, copy) NSArray<FIRDocumentSnapshot *> *documents;
/// Only used for the "set data" type
@property (nonatomic, readonly) FLEXFirebaseSetDataInfo *setDataInfo;
/// Only used for the "update data" type
@property (nonatomic, readonly) NSDictionary *updateData;
/// Only used for the "add document" type
@property (nonatomic, readonly) FIRDocumentReference *addedDocument;
@property (nonatomic, readonly) NSString *path;
//@property (nonatomic, readonly) NSString *responseString;
//@property (nonatomic, readonly) NSDictionary *responseObject;
@end

View File

@@ -0,0 +1,295 @@
//
// FLEXNetworkTransaction.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXNetworkTransaction.h"
#import "FLEXResources.h"
#import "FLEXUtility.h"
@implementation FLEXNetworkTransaction
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state {
NSString *readableString = nil;
switch (state) {
case FLEXNetworkTransactionStateUnstarted:
readableString = @"Unstarted";
break;
case FLEXNetworkTransactionStateAwaitingResponse:
readableString = @"Awaiting Response";
break;
case FLEXNetworkTransactionStateReceivingData:
readableString = @"Receiving Data";
break;
case FLEXNetworkTransactionStateFinished:
readableString = @"Finished";
break;
case FLEXNetworkTransactionStateFailed:
readableString = @"Failed";
break;
}
return readableString;
}
+ (instancetype)withStartTime:(NSDate *)startTime {
FLEXNetworkTransaction *transaction = [self new];
transaction->_startTime = startTime;
return transaction;
}
- (NSString *)timestampStringFromRequestDate:(NSDate *)date {
static NSDateFormatter *dateFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = @"HH:mm:ss";
});
return [dateFormatter stringFromDate:date];
}
- (void)setState:(FLEXNetworkTransactionState)transactionState {
_state = transactionState;
// Reset bottom description
_tertiaryDescription = nil;
}
- (BOOL)displayAsError {
return _error != nil;
}
- (NSString *)copyString {
return nil;
}
- (BOOL)matchesQuery:(NSString *)filterString {
return NO;
}
@end
@interface FLEXURLTransaction ()
@end
@implementation FLEXURLTransaction
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime {
FLEXURLTransaction *transaction = [self withStartTime:startTime];
transaction->_request = request;
return transaction;
}
- (NSString *)primaryDescription {
if (!_primaryDescription) {
NSString *name = self.request.URL.lastPathComponent;
if (!name.length) {
name = @"/";
}
if (_request.URL.query) {
name = [name stringByAppendingFormat:@"?%@", self.request.URL.query];
}
_primaryDescription = name;
}
return _primaryDescription;
}
- (NSString *)secondaryDescription {
if (!_secondaryDescription) {
NSMutableArray<NSString *> *mutablePathComponents = self.request.URL.pathComponents.mutableCopy;
if (mutablePathComponents.count > 0) {
[mutablePathComponents removeLastObject];
}
NSString *path = self.request.URL.host;
for (NSString *pathComponent in mutablePathComponents) {
path = [path stringByAppendingPathComponent:pathComponent];
}
_secondaryDescription = path;
}
return _secondaryDescription;
}
- (NSString *)tertiaryDescription {
if (!_tertiaryDescription) {
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
NSString *timestamp = [self timestampStringFromRequestDate:self.startTime];
if (timestamp.length > 0) {
[detailComponents addObject:timestamp];
}
// Omit method for GET (assumed as default)
NSString *httpMethod = self.request.HTTPMethod;
if (httpMethod.length > 0) {
[detailComponents addObject:httpMethod];
}
if (self.state == FLEXNetworkTransactionStateFinished || self.state == FLEXNetworkTransactionStateFailed) {
[detailComponents addObjectsFromArray:self.details];
} else {
// Unstarted, Awaiting Response, Receiving Data, etc.
NSString *state = [self.class readableStringFromTransactionState:self.state];
[detailComponents addObject:state];
}
_tertiaryDescription = [detailComponents componentsJoinedByString:@" ・ "];
}
return _tertiaryDescription;
}
- (NSString *)copyString {
return self.request.URL.absoluteString;
}
- (BOOL)matchesQuery:(NSString *)filterString {
return [self.request.URL.absoluteString localizedCaseInsensitiveContainsString:filterString];
}
@end
@interface FLEXHTTPTransaction ()
@property (nonatomic, readwrite) NSData *cachedRequestBody;
@end
@implementation FLEXHTTPTransaction
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID {
FLEXHTTPTransaction *httpt = [self withRequest:request startTime:NSDate.date];
httpt->_requestID = requestID;
return httpt;
}
- (NSString *)description {
NSString *description = [super description];
description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
return description;
}
- (NSData *)cachedRequestBody {
if (!_cachedRequestBody) {
if (self.request.HTTPBody != nil) {
_cachedRequestBody = self.request.HTTPBody;
} else if ([self.request.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) {
NSInputStream *bodyStream = [self.request.HTTPBodyStream copy];
const NSUInteger bufferSize = 1024;
uint8_t buffer[bufferSize];
NSMutableData *data = [NSMutableData new];
[bodyStream open];
NSInteger readBytes = 0;
do {
readBytes = [bodyStream read:buffer maxLength:bufferSize];
[data appendBytes:buffer length:readBytes];
} while (readBytes > 0);
[bodyStream close];
_cachedRequestBody = data;
}
}
return _cachedRequestBody;
}
- (NSArray *)detailString {
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.response];
if (statusCodeString.length > 0) {
[detailComponents addObject:statusCodeString];
}
if (self.receivedDataLength > 0) {
NSString *responseSize = [NSByteCountFormatter
stringFromByteCount:self.receivedDataLength
countStyle:NSByteCountFormatterCountStyleBinary
];
[detailComponents addObject:responseSize];
}
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.duration];
NSString *latency = [FLEXUtility stringFromRequestDuration:self.latency];
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
[detailComponents addObject:duration];
return detailComponents;
}
- (BOOL)displayAsError {
return [FLEXUtility isErrorStatusCodeFromURLResponse:self.response] || super.displayAsError;
}
@end
@implementation FLEXWebsocketTransaction
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task
direction:(FLEXWebsocketMessageDirection)direction
startTime:(NSDate *)started {
FLEXWebsocketTransaction *wst = [self withRequest:task.originalRequest startTime:started];
wst->_message = message;
wst->_direction = direction;
// Populate receivedDataLength
if (direction == FLEXWebsocketIncoming) {
wst.receivedDataLength = wst.dataLength;
wst.state = FLEXNetworkTransactionStateFinished;
}
// Populate thumbnail image
if (message.type == NSURLSessionWebSocketMessageTypeData) {
wst.thumbnail = FLEXResources.binaryIcon;
} else {
wst.thumbnail = FLEXResources.textIcon;
}
return wst;
}
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
task:(NSURLSessionWebSocketTask *)task
direction:(FLEXWebsocketMessageDirection)direction {
return [self withMessage:message task:task direction:direction startTime:NSDate.date];
}
- (NSArray<NSString *> *)details API_AVAILABLE(ios(13.0)) {
return @[
self.direction == FLEXWebsocketOutgoing ? @"SENT →" : @"→ RECEIVED",
[NSByteCountFormatter
stringFromByteCount:self.dataLength
countStyle:NSByteCountFormatterCountStyleBinary
]
];
}
- (int64_t)dataLength {
if (self.message) {
if (self.message.type == NSURLSessionWebSocketMessageTypeString) {
return self.message.string.length;
}
return self.message.data.length;
}
return 0;
}
@end

View File

@@ -0,0 +1,20 @@
//
// FLEXNetworkTransactionCell.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXNetworkTransaction;
@interface FLEXNetworkTransactionCell : UITableViewCell
@property (nonatomic) FLEXNetworkTransaction *transaction;
@property (nonatomic, readonly, class) NSString *reuseID;
@property (nonatomic, readonly, class) CGFloat preferredCellHeight;
@end

View File

@@ -0,0 +1,116 @@
//
// FLEXNetworkTransactionCell.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXNetworkTransactionCell.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
NSString * const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
@interface FLEXNetworkTransactionCell ()
@property (nonatomic) UIImageView *thumbnailImageView;
@property (nonatomic) UILabel *nameLabel;
@property (nonatomic) UILabel *pathLabel;
@property (nonatomic) UILabel *transactionDetailsLabel;
@end
@implementation FLEXNetworkTransactionCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
self.nameLabel = [UILabel new];
self.nameLabel.font = UIFont.flex_defaultTableCellFont;
[self.contentView addSubview:self.nameLabel];
self.pathLabel = [UILabel new];
self.pathLabel.font = UIFont.flex_defaultTableCellFont;
self.pathLabel.textColor = [UIColor colorWithWhite:0.4 alpha:1.0];
self.pathLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
[self.contentView addSubview:self.pathLabel];
self.thumbnailImageView = [UIImageView new];
self.thumbnailImageView.layer.borderColor = UIColor.blackColor.CGColor;
self.thumbnailImageView.layer.borderWidth = 1.0;
self.thumbnailImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:self.thumbnailImageView];
self.transactionDetailsLabel = [UILabel new];
self.transactionDetailsLabel.font = [UIFont systemFontOfSize:10.0];
self.transactionDetailsLabel.textColor = [UIColor colorWithWhite:0.65 alpha:1.0];
[self.contentView addSubview:self.transactionDetailsLabel];
}
return self;
}
- (void)setTransaction:(FLEXNetworkTransaction *)transaction {
if (_transaction != transaction) {
_transaction = transaction;
[self setNeedsLayout];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
const CGFloat kVerticalPadding = 8.0;
const CGFloat kLeftPadding = 10.0;
const CGFloat kImageDimension = 32.0;
CGFloat thumbnailOriginY = round((self.contentView.bounds.size.height - kImageDimension) / 2.0);
self.thumbnailImageView.frame = CGRectMake(kLeftPadding, thumbnailOriginY, kImageDimension, kImageDimension);
self.thumbnailImageView.image = self.transaction.thumbnail;
CGFloat textOriginX = CGRectGetMaxX(self.thumbnailImageView.frame) + kLeftPadding;
CGFloat availableTextWidth = self.contentView.bounds.size.width - textOriginX;
self.nameLabel.text = [self nameLabelText];
CGSize nameLabelPreferredSize = [self.nameLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
self.nameLabel.frame = CGRectMake(textOriginX, kVerticalPadding, availableTextWidth, nameLabelPreferredSize.height);
self.nameLabel.textColor = self.transaction.displayAsError ? UIColor.redColor : FLEXColor.primaryTextColor;
self.pathLabel.text = [self pathLabelText];
CGSize pathLabelPreferredSize = [self.pathLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
CGFloat pathLabelOriginY = ceil((self.contentView.bounds.size.height - pathLabelPreferredSize.height) / 2.0);
self.pathLabel.frame = CGRectMake(textOriginX, pathLabelOriginY, availableTextWidth, pathLabelPreferredSize.height);
self.transactionDetailsLabel.text = [self transactionDetailsLabelText];
CGSize transactionLabelPreferredSize = [self.transactionDetailsLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
CGFloat transactionDetailsOriginX = textOriginX;
CGFloat transactionDetailsLabelOriginY = CGRectGetMaxY(self.contentView.bounds) - kVerticalPadding - transactionLabelPreferredSize.height;
CGFloat transactionDetailsLabelWidth = self.contentView.bounds.size.width - transactionDetailsOriginX;
self.transactionDetailsLabel.frame = CGRectMake(transactionDetailsOriginX, transactionDetailsLabelOriginY, transactionDetailsLabelWidth, transactionLabelPreferredSize.height);
}
- (NSString *)nameLabelText {
return self.transaction.primaryDescription;
}
- (NSString *)pathLabelText {
return self.transaction.secondaryDescription;
}
- (NSString *)transactionDetailsLabelText {
return self.transaction.tertiaryDescription;
}
+ (CGFloat)preferredCellHeight {
return 65.0;
}
+ (NSString *)reuseID {
return kFLEXNetworkTransactionCellIdentifier;
}
@end

View File

@@ -0,0 +1,190 @@
//
// Firestore.h
// Pods
//
// Created by Tanner Bennett on 10/13/21.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Forward Declarations
@class FIRQuery;
@class FIRQuerySnapshot;
@class FIRDocumentReference;
@class FIRDocumentSnapshot;
@class FIRQueryDocumentSnapshot;
@class FIRCollectionReference;
@class FIRFirestore;
@protocol FIRListenerRegistration;
#define cFIRQuery objc_getClass("FIRQuery")
#define cFIRCollectionReference objc_getClass("FIRCollectionReference")
#define cFIRDocumentReference objc_getClass("FIRDocumentReference")
typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot,
NSError *_Nullable error);
typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot,
NSError *_Nullable error);
typedef NS_ENUM(NSUInteger, FIRFirestoreSource) {
FIRFirestoreSourceDefault,
FIRFirestoreSourceServer,
FIRFirestoreSourceCache
} NS_SWIFT_NAME(FirestoreSource);
#pragma mark - Query
@interface FIRQuery : NSObject
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) FIRFirestore *firestore;
@property(nonatomic, readonly) void *query;
- (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion
NS_SWIFT_NAME(getDocuments(completion:));
- (void)getDocumentsWithSource:(FIRFirestoreSource)source
completion:(FIRQuerySnapshotBlock)completion
NS_SWIFT_NAME(getDocuments(source:completion:));
@end
typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot,
NSError *_Nullable error);
#pragma mark - DocumentReference
NS_SWIFT_NAME(DocumentReference)
@interface FIRDocumentReference : NSObject
- (instancetype)init __attribute__((unavailable));
@property(nonatomic, readonly) NSString *documentID;
@property(nonatomic, readonly) FIRCollectionReference *parent;
@property(nonatomic, readonly) FIRFirestore *firestore;
@property(nonatomic, readonly) NSString *path;
- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
NS_SWIFT_NAME(collection(_:));
#pragma mark Writing Data
- (void)setData:(NSDictionary<NSString *, id> *)documentData;
- (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge;
- (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
merge:(BOOL)merge
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
mergeFields:(NSArray<id> *)mergeFields
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)updateData:(NSDictionary<id, id> *)fields;
- (void)updateData:(NSDictionary<id, id> *)fields
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)deleteDocument NS_SWIFT_NAME(delete());
- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion
NS_SWIFT_NAME(delete(completion:));
#pragma mark Retrieving Data
- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion
NS_SWIFT_NAME(getDocument(completion:));
- (void)getDocumentWithSource:(FIRFirestoreSource)source
completion:(FIRDocumentSnapshotBlock)completion
NS_SWIFT_NAME(getDocument(source:completion:));
- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener
NS_SWIFT_NAME(addSnapshotListener(_:));
- (id<FIRListenerRegistration>)addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
listener:(FIRDocumentSnapshotBlock)listener
NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:));
@end
#pragma mark - CollectionReference
NS_SWIFT_NAME(CollectionReference)
@interface FIRCollectionReference : FIRQuery
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) NSString *collectionID;
@property(nonatomic, nullable, readonly) FIRDocumentReference *parent;
@property(nonatomic, readonly) NSString *path;
- (FIRDocumentReference *)documentWithAutoID NS_SWIFT_NAME(document());
- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath NS_SWIFT_NAME(document(_:));
- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
NS_SWIFT_NAME(addDocument(data:));
- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
completion:(nullable void (^)(NSError *_Nullable error))completion
NS_SWIFT_NAME(addDocument(data:completion:));
@end
#pragma mark - QuerySnapshot
NS_SWIFT_NAME(QuerySnapshot)
@interface FIRQuerySnapshot : NSObject
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) FIRQuery *query;
@property(nonatomic, readonly, getter=isEmpty) BOOL empty;
@property(nonatomic, readonly) NSInteger count;
@property(nonatomic, readonly) NSArray<FIRQueryDocumentSnapshot *> *documents;
@end
#pragma mark - DocumentSnapshot
NS_SWIFT_NAME(DocumentSnapshot)
@interface FIRDocumentSnapshot : NSObject
- (instancetype)init __attribute__((unavailable()));
@property(nonatomic, readonly) BOOL exists;
@property(nonatomic, readonly) FIRDocumentReference *reference;
@property(nonatomic, copy, readonly) NSString *documentID;
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, id> *data;
- (nullable id)valueForField:(id)field NS_SWIFT_NAME(get(_:));
- (nullable id)objectForKeyedSubscript:(id)key;
@end
#pragma mark - QueryDocumentSnapshot
NS_SWIFT_NAME(QueryDocumentSnapshot)
@interface FIRQueryDocumentSnapshot : FIRDocumentSnapshot
- (instancetype)init __attribute__((unavailable()));
@property(nonatomic, readonly) NSDictionary<NSString *, id> *data;
@end
NS_ASSUME_NONNULL_END
#if defined(__clang__)
#if __has_feature(objc_arc)
#define _LOGOS_SELF_TYPE_NORMAL __unsafe_unretained
#define _LOGOS_SELF_TYPE_INIT __attribute__((ns_consumed))
#define _LOGOS_SELF_CONST const
#define _LOGOS_RETURN_RETAINED __attribute__((ns_returns_retained))
#else
#define _LOGOS_SELF_TYPE_NORMAL
#define _LOGOS_SELF_TYPE_INIT
#define _LOGOS_SELF_CONST
#define _LOGOS_RETURN_RETAINED
#endif
#else
#define _LOGOS_SELF_TYPE_NORMAL
#define _LOGOS_SELF_TYPE_INIT
#define _LOGOS_SELF_CONST
#define _LOGOS_RETURN_RETAINED
#endif

View File

@@ -0,0 +1,20 @@
OSCache
version 1.2.1, Decembet 18th, 2015
Copyright (C) 2014 Charcoal Design
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

View File

@@ -0,0 +1,57 @@
//
// OSCache.h
//
// Version 1.2.1
//
// Created by Nick Lockwood on 01/01/2014.
// Copyright (C) 2014 Charcoal Design
//
// Distributed under the permissive zlib License
// Get the latest version from here:
//
// https://github.com/nicklockwood/OSCache
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface OSCache <KeyType, ObjectType> : NSCache <NSFastEnumeration>
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) NSUInteger totalCost;
- (id)objectForKeyedSubscript:(KeyType <NSCopying>)key;
- (void)setObject:(ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key;
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;
@end
@protocol OSCacheDelegate <NSCacheDelegate>
@optional
- (BOOL)cache:(OSCache *)cache shouldEvictObject:(id)entry;
- (void)cache:(OSCache *)cache willEvictObject:(id)entry;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,409 @@
//
// OSCache.m
//
// Version 1.2.1
//
// Created by Nick Lockwood on 01/01/2014.
// Copyright (C) 2014 Charcoal Design
//
// Distributed under the permissive zlib License
// Get the latest version from here:
//
// https://github.com/nicklockwood/OSCache
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
#import "OSCache.h"
#import <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#endif
#import <Availability.h>
#if !__has_feature(objc_arc)
#error This class requires automatic reference counting
#endif
#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis"
#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
#pragma GCC diagnostic ignored "-Wgnu"
@interface OSCacheEntry : NSObject
@property (nonatomic, strong) NSObject *object;
@property (nonatomic, assign) NSUInteger cost;
@property (nonatomic, assign) NSInteger sequenceNumber;
@end
@implementation OSCacheEntry
@end
@interface OSCache_Private : NSObject
@property (nonatomic, unsafe_unretained) id<OSCacheDelegate> delegate;
@property (nonatomic, assign) NSUInteger countLimit;
@property (nonatomic, assign) NSUInteger totalCostLimit;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableDictionary *cache;
@property (nonatomic, assign) NSUInteger totalCost;
@property (nonatomic, assign) NSInteger sequenceNumber;
@end
@implementation OSCache_Private
{
BOOL _delegateRespondsToWillEvictObject;
BOOL _delegateRespondsToShouldEvictObject;
BOOL _currentlyCleaning;
NSMutableArray *_entryPool;
NSLock *_lock;
}
- (instancetype)init
{
if ((self = [super init]))
{
//create storage
_cache = [[NSMutableDictionary alloc] init];
_entryPool = [[NSMutableArray alloc] init];
_lock = [[NSLock alloc] init];
_totalCost = 0;
#if TARGET_OS_IPHONE
//clean up in the event of a memory warning
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanUpAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setDelegate:(id<OSCacheDelegate>)delegate
{
_delegate = delegate;
_delegateRespondsToShouldEvictObject = [delegate respondsToSelector:@selector(cache:shouldEvictObject:)];
_delegateRespondsToWillEvictObject = [delegate respondsToSelector:@selector(cache:willEvictObject:)];
}
- (void)setCountLimit:(NSUInteger)countLimit
{
[_lock lock];
_countLimit = countLimit;
[_lock unlock];
[self cleanUp:NO];
}
- (void)setTotalCostLimit:(NSUInteger)totalCostLimit
{
[_lock lock];
_totalCostLimit = totalCostLimit;
[_lock unlock];
[self cleanUp:NO];
}
- (NSUInteger)count
{
return [_cache count];
}
- (void)cleanUp:(BOOL)keepEntries
{
[_lock lock];
NSUInteger maxCount = _countLimit ?: INT_MAX;
NSUInteger maxCost = _totalCostLimit ?: INT_MAX;
NSUInteger totalCount = _cache.count;
NSMutableArray *keys = [_cache.allKeys mutableCopy];
while (totalCount > maxCount || _totalCost > maxCost)
{
NSInteger lowestSequenceNumber = INT_MAX;
OSCacheEntry *lowestEntry = nil;
id lowestKey = nil;
//remove oldest items until within limit
for (id key in keys)
{
OSCacheEntry *entry = _cache[key];
if (entry.sequenceNumber < lowestSequenceNumber)
{
lowestSequenceNumber = entry.sequenceNumber;
lowestEntry = entry;
lowestKey = key;
}
}
if (lowestKey)
{
[keys removeObject:lowestKey];
if (!_delegateRespondsToShouldEvictObject ||
[_delegate cache:(OSCache *)self shouldEvictObject:lowestEntry.object])
{
if (_delegateRespondsToWillEvictObject)
{
_currentlyCleaning = YES;
[self.delegate cache:(OSCache *)self willEvictObject:lowestEntry.object];
_currentlyCleaning = NO;
}
[_cache removeObjectForKey:lowestKey];
_totalCost -= lowestEntry.cost;
totalCount --;
if (keepEntries)
{
[_entryPool addObject:lowestEntry];
lowestEntry.object = nil;
}
}
}
}
[_lock unlock];
}
- (void)cleanUpAllObjects
{
[_lock lock];
if (_delegateRespondsToShouldEvictObject || _delegateRespondsToWillEvictObject)
{
NSArray *keys = [_cache allKeys];
if (_delegateRespondsToShouldEvictObject)
{
//sort, oldest first (in case we want to use that information in our eviction test)
keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) {
OSCacheEntry *entry1 = self->_cache[key1];
OSCacheEntry *entry2 = self->_cache[key2];
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
}];
}
//remove all items individually
for (id key in keys)
{
OSCacheEntry *entry = _cache[key];
if (!_delegateRespondsToShouldEvictObject || [_delegate cache:(OSCache *)self shouldEvictObject:entry.object])
{
if (_delegateRespondsToWillEvictObject)
{
_currentlyCleaning = YES;
[_delegate cache:(OSCache *)self willEvictObject:entry.object];
_currentlyCleaning = NO;
}
[_cache removeObjectForKey:key];
_totalCost -= entry.cost;
}
}
}
else
{
_totalCost = 0;
[_cache removeAllObjects];
_sequenceNumber = 0;
}
[_lock unlock];
}
- (void)resequence
{
//sort, oldest first
NSArray *entries = [[_cache allValues] sortedArrayUsingComparator:^NSComparisonResult(OSCacheEntry *entry1, OSCacheEntry *entry2) {
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
}];
//renumber items
NSInteger index = 0;
for (OSCacheEntry *entry in entries)
{
entry.sequenceNumber = index++;
}
}
- (id)objectForKey:(id)key
{
[_lock lock];
OSCacheEntry *entry = _cache[key];
entry.sequenceNumber = _sequenceNumber++;
if (_sequenceNumber < 0)
{
[self resequence];
}
id object = entry.object;
[_lock unlock];
return object;
}
- (id)objectForKeyedSubscript:(id<NSCopying>)key
{
return [self objectForKey:key];
}
- (void)setObject:(id)obj forKey:(id)key
{
[self setObject:obj forKey:key cost:0];
}
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
{
[self setObject:obj forKey:key cost:0];
}
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
{
if (!obj)
{
[self removeObjectForKey:key];
return;
}
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
[_lock lock];
_totalCost -= [_cache[key] cost];
_totalCost += g;
OSCacheEntry *entry = _cache[key];
if (!entry) {
entry = [[OSCacheEntry alloc] init];
_cache[key] = entry;
}
entry.object = obj;
entry.cost = g;
entry.sequenceNumber = _sequenceNumber++;
if (_sequenceNumber < 0)
{
[self resequence];
}
[_lock unlock];
[self cleanUp:YES];
}
- (void)removeObjectForKey:(id)key
{
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
[_lock lock];
OSCacheEntry *entry = _cache[key];
if (entry) {
_totalCost -= entry.cost;
entry.object = nil;
[_entryPool addObject:entry];
[_cache removeObjectForKey:key];
}
[_lock unlock];
}
- (void)removeAllObjects
{
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
[_lock lock];
_totalCost = 0;
_sequenceNumber = 0;
for (OSCacheEntry *entry in _cache.allValues)
{
entry.object = nil;
[_entryPool addObject:entry];
}
[_cache removeAllObjects];
[_lock unlock];
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained [])buffer
count:(NSUInteger)len
{
[_lock lock];
NSUInteger count = [_cache countByEnumeratingWithState:state objects:buffer count:len];
[_lock unlock];
return count;
}
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
if (block)
{
[_lock lock];
[_cache enumerateKeysAndObjectsUsingBlock:^(id key, OSCacheEntry *entry, BOOL *stop) {
block(key, entry.object, stop);
}];
[_lock unlock];
}
}
//handle unimplemented methods
- (BOOL)isKindOfClass:(Class)aClass
{
//pretend that we're an NSCache if anyone asks
if (aClass == [OSCache class] || aClass == [NSCache class])
{
return YES;
}
return [super isKindOfClass:aClass];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
//protect against calls to unimplemented NSCache methods
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (!signature)
{
signature = [NSCache instanceMethodSignatureForSelector:selector];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
[invocation invokeWithTarget:nil];
#pragma clang diagnostic pop
}
@end
@implementation OSCache
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return (OSCache *)[OSCache_Private allocWithZone:zone];
}
- (id)objectForKeyedSubscript:(__unused id<NSCopying>)key { return nil; }
- (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id<NSCopying>)key {}
- (void)enumerateKeysAndObjectsUsingBlock:(__unused void (^)(id, id, BOOL *))block { }
- (NSUInteger)countByEnumeratingWithState:(__unused NSFastEnumerationState *)state
objects:(__unused __unsafe_unretained id [])buffer
count:(__unused NSUInteger)len { return 0; }
@end

View File

@@ -0,0 +1,28 @@
//
// FLEXNetworkObserver.h
// Derived from:
//
// PDAFNetworkDomainController.h
// PonyDebugger
//
// Created by Mike Lewis on 2/27/12.
//
// Licensed to Square, Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Square, Inc. licenses this file to you.
//
#import <Foundation/Foundation.h>
FOUNDATION_EXTERN NSString *const kFLEXNetworkObserverEnabledStateChangedNotification;
/// This class swizzles NSURLConnection and NSURLSession delegate methods to observe events in the URL loading system.
/// High level network events are sent to the default FLEXNetworkRecorder instance which maintains the request history and caches response bodies.
@interface FLEXNetworkObserver : NSObject
/// Swizzling occurs when the observer is enabled for the first time.
/// This reduces the impact of FLEX if network debugging is not desired.
/// NOTE: this setting persists between launches of the app.
@property (nonatomic, class, getter=isEnabled) BOOL enabled;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
PonyDebugger
Copyright 2012 Square Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.