mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-12-27 00:47:20 -05:00
added files via upload
This commit is contained in:
303
Tweaks/FLEX/Network/FLEXFirebaseTransaction.mm
Normal file
303
Tweaks/FLEX/Network/FLEXFirebaseTransaction.mm
Normal 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
|
||||
17
Tweaks/FLEX/Network/FLEXHTTPTransactionDetailController.h
Normal file
17
Tweaks/FLEX/Network/FLEXHTTPTransactionDetailController.h
Normal 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
|
||||
535
Tweaks/FLEX/Network/FLEXHTTPTransactionDetailController.m
Normal file
535
Tweaks/FLEX/Network/FLEXHTTPTransactionDetailController.m
Normal 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
|
||||
33
Tweaks/FLEX/Network/FLEXMITMDataSource.h
Normal file
33
Tweaks/FLEX/Network/FLEXMITMDataSource.h
Normal 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
|
||||
104
Tweaks/FLEX/Network/FLEXMITMDataSource.m
Normal file
104
Tweaks/FLEX/Network/FLEXMITMDataSource.m
Normal 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
|
||||
19
Tweaks/FLEX/Network/FLEXNetworkCurlLogger.h
Normal file
19
Tweaks/FLEX/Network/FLEXNetworkCurlLogger.h
Normal 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
|
||||
38
Tweaks/FLEX/Network/FLEXNetworkCurlLogger.m
Normal file
38
Tweaks/FLEX/Network/FLEXNetworkCurlLogger.m
Normal 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
|
||||
15
Tweaks/FLEX/Network/FLEXNetworkMITMViewController.h
Normal file
15
Tweaks/FLEX/Network/FLEXNetworkMITMViewController.h
Normal 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
|
||||
633
Tweaks/FLEX/Network/FLEXNetworkMITMViewController.m
Normal file
633
Tweaks/FLEX/Network/FLEXNetworkMITMViewController.m
Normal 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
|
||||
122
Tweaks/FLEX/Network/FLEXNetworkRecorder.h
Normal file
122
Tweaks/FLEX/Network/FLEXNetworkRecorder.h
Normal 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
|
||||
500
Tweaks/FLEX/Network/FLEXNetworkRecorder.m
Normal file
500
Tweaks/FLEX/Network/FLEXNetworkRecorder.m
Normal 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
|
||||
12
Tweaks/FLEX/Network/FLEXNetworkSettingsController.h
Normal file
12
Tweaks/FLEX/Network/FLEXNetworkSettingsController.h
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// FLEXNetworkSettingsController.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 2/20/15.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXNetworkSettingsController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
253
Tweaks/FLEX/Network/FLEXNetworkSettingsController.m
Normal file
253
Tweaks/FLEX/Network/FLEXNetworkSettingsController.m
Normal 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
|
||||
178
Tweaks/FLEX/Network/FLEXNetworkTransaction.h
Normal file
178
Tweaks/FLEX/Network/FLEXNetworkTransaction.h
Normal 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
|
||||
295
Tweaks/FLEX/Network/FLEXNetworkTransaction.m
Normal file
295
Tweaks/FLEX/Network/FLEXNetworkTransaction.m
Normal 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
|
||||
20
Tweaks/FLEX/Network/FLEXNetworkTransactionCell.h
Normal file
20
Tweaks/FLEX/Network/FLEXNetworkTransactionCell.h
Normal 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
|
||||
116
Tweaks/FLEX/Network/FLEXNetworkTransactionCell.m
Normal file
116
Tweaks/FLEX/Network/FLEXNetworkTransactionCell.m
Normal 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
|
||||
190
Tweaks/FLEX/Network/Firestore.h
Normal file
190
Tweaks/FLEX/Network/Firestore.h
Normal 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
|
||||
20
Tweaks/FLEX/Network/OSCache/LICENSE.md
Normal file
20
Tweaks/FLEX/Network/OSCache/LICENSE.md
Normal 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.
|
||||
57
Tweaks/FLEX/Network/OSCache/OSCache.h
Normal file
57
Tweaks/FLEX/Network/OSCache/OSCache.h
Normal 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
|
||||
409
Tweaks/FLEX/Network/OSCache/OSCache.m
Normal file
409
Tweaks/FLEX/Network/OSCache/OSCache.m
Normal 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
|
||||
28
Tweaks/FLEX/Network/PonyDebugger/FLEXNetworkObserver.h
Normal file
28
Tweaks/FLEX/Network/PonyDebugger/FLEXNetworkObserver.h
Normal 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
|
||||
1997
Tweaks/FLEX/Network/PonyDebugger/FLEXNetworkObserver.m
Normal file
1997
Tweaks/FLEX/Network/PonyDebugger/FLEXNetworkObserver.m
Normal file
File diff suppressed because it is too large
Load Diff
16
Tweaks/FLEX/Network/PonyDebugger/LICENSE
Normal file
16
Tweaks/FLEX/Network/PonyDebugger/LICENSE
Normal 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.
|
||||
|
||||
Reference in New Issue
Block a user