mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04: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
	 Balackburn
					Balackburn