Compare commits

..

151 Commits

Author SHA1 Message Date
Tanner Bennett 2bfba6715e Fix indentation of these #if's
VS Code handles folding with regex so these need to be indented properly or it will not let you fold the surroudning context
2024-08-25 04:36:28 -05:00
Tanner Bennett 0c87d94b0d Fix some warnings 2024-08-25 04:33:09 -05:00
Sergio Sette 401a94f81b Fix cURL null payload for compressed HTTP bodies 2024-08-25 03:47:50 -05:00
Quan f92dd73442 Add Surmagic surfile 2024-08-25 03:45:07 -05:00
linzuhan 3ccf4b8c59 Fix #672 misaligned UITableView row height 2024-08-25 03:45:07 -05:00
Tanner Bennett e036f964d8 Fix #680, a crash when viewing nil HTTP body 2024-08-25 03:22:43 -05:00
Tanner Bennett a1c3e86ce7 Clean up deprecated APIs 2024-08-25 03:11:44 -05:00
helloworld 079f2d87a5 Fix main branch build error (#703)
Co-authored-by: lichenguang <chenguang.li@mail.dealmoon.com>
2024-06-27 02:53:24 -07:00
Nikolay Morev 462c507df9 Make FLEXFileBrowserController public (#702) 2024-06-19 21:30:34 -05:00
Ata Etgi a19f1acbed fix the crash when opening view debug hierarchy (#700) 2024-06-19 20:50:11 -05:00
Tanner Bennett 1b983160cc Attempt to fix crash in 3D view explorer
+[FHSView drawView:] may crash when the view has 0 width or height
2024-02-28 11:14:34 -08:00
Cao Doan 6616242d9c Migrate NSKeyedArchiver/NSKeyedUnarchiver deprecated APIs (#693) 2024-02-16 01:13:13 -06:00
Mykola (Nickolas) Pokhylets 377a9a5f3f Handle HTTP body which is not a UTF8 text (#684)
Co-authored-by: Mykola Pokhylets <mykola.pokhylets@gmail.com>
2024-02-12 03:38:05 -06:00
Brovko Roman 6f4d9c3ba1 Fix crash in FLEXNetworkObserver (#692)
Thanks @programming086!
2024-02-12 03:33:21 -06:00
Míng 56935533be Supports displaying and searching by accessibilityIdentifier in the Hierarchy view. (#690) 2024-02-05 11:22:46 -06:00
Rob Napier f0b2dc971e Ignore AVAssetDownloadURLSession to avoid crashing. (FLEXTool#276) (#677) 2024-02-03 12:52:06 -06:00
Enrique Garcia 346301b628 Fix Xcode 15 SPM warning related to iOS 12 support (#688) 2024-02-03 12:45:47 -06:00
Ingrid Wang 692146831e Use requiredProperties and optionalProperties in debug description (#687) 2024-01-30 17:31:25 -06:00
Ahmed Hamdy 81f748a33e Fix EXC_BAD_ACCESS in FLEXNetworkObserver (#660)
---------

Co-authored-by: ahmed hamdy <ahamdy@lavad.app>
2023-03-09 18:33:07 -06:00
KeepMoving 038106f954 Fix crash caused by metadata name being nil (#659)
Co-authored-by: linzuhan <lzh267448@alibaba-inc.com>
2023-03-07 21:05:59 -06:00
Emil Pedersen 1bbf8dbea1 Locale aware animation speed parsing (#642) 2022-12-09 03:33:07 -06:00
Tanner Bennett 343eba92b6 Finally fix the order of these buttons 2022-12-05 18:09:13 -06:00
Tanner Bennett d6232baf03 Give user opportunity to edit bad queries 2022-12-05 18:09:13 -06:00
Tanner Bennett 9808b89d6d Enable preferred actions with FLEXAlert 2022-12-05 17:55:40 -06:00
Asif de1cbfae4a Fix response body not in cache issue (#640) 2022-11-29 20:34:19 -08:00
Tanner Bennett b036fb18a5 Make class sizes only populate instansiated classes 2022-11-29 16:52:29 -06:00
Tanner Bennett c6ed5098cf Xcode 14 upgrade check 2022-11-22 04:02:40 -06:00
Tanner Bennett d7283cd22d Add FLEXHeapSnapshot 2022-11-22 04:02:14 -06:00
Tanner Bennett 785e181d75 Move subclassesOfClassWithName: to FLEXRuntimeUtility 2022-11-22 04:02:07 -06:00
Tanner Bennett dd66f46a7b Add docs to some APIs 2022-11-22 04:01:28 -06:00
Tanner Bennett 55bae8b53e Make some new headers public; fix #636 2022-11-22 03:14:39 -06:00
Tanner Bennett 5edca3fdad Update Reflex dependency URL; fix #617 2022-11-22 02:56:50 -06:00
Tanner Bennett 9bc319a38e Refine FLEXWindow level; fix #541 2022-11-22 02:47:03 -06:00
Tanner Bennett deac628b06 None of these should be optional 2022-11-04 21:19:16 -05:00
Tanner Bennett 8e44d88c36 Remove social_media_url 2022-10-30 03:09:14 -05:00
Tanner Bennett 73e4a712e8 Refine shouldReceiveTouchAtWindowPoint: logic 2022-10-20 12:50:48 -05:00
Tanner Bennett 9e1f60b2f4 "Recent" button should be disabled in presentation 2022-10-20 12:50:48 -05:00
Tanner Bennett 3c385143ee Use half sheet on iOS 15+ 2022-10-20 12:50:48 -05:00
Tanner Bennett e0acf59d06 Bump version; no longer using semantic versioning
Now versioned using `marketing-year-month`
2022-10-20 12:41:01 -05:00
Tanner Bennett f00af28418 Big brain refactor 2022-10-20 12:29:21 -05:00
Tanner Bennett 2bd809ae6c Xcode 14 2022-10-20 12:29:21 -05:00
Tanner Bennett 003b8f88e8 Add FLEXObjectRef.debugDescription 2022-10-20 12:29:21 -05:00
Tanner Bennett 5e780e6d0c Fix UIButton selection view on iOS 16 2022-10-20 12:29:21 -05:00
Tanner Bennett 17ea4976c1 Fix potential retain cycle 2022-10-10 23:10:51 -04:00
Tanner Bennett fbda722f11 Fix bug in supportedInterfaceOrientations 2022-10-09 02:39:44 -04:00
Iulian Onofrei b351b788d5 Fix error-prone FLEX exclusion from release instruction (#630) 2022-10-07 11:39:45 -04:00
Tanner Bennett a195840b67 I don't think this actually fixes anything but oh well 2022-10-06 23:01:31 -04:00
Tanner Bennett 9b946dda5d Fix crash when deleting last blacklisted host 2022-10-06 23:00:47 -04:00
Tanner Bennett 806cdc6acb Compatibility fix for new version of Theos 2022-10-06 00:13:10 -04:00
Tanner Bennett aadfc69f01 Add FLEXWindowShortcuts to project 2022-10-06 00:07:01 -04:00
Tanner Bennett 6e0135d4a0 Fix #627 2022-10-01 15:32:23 -05:00
Tanner Bennett 2599a1eb0d dogs.realm was not being included in example app 2022-10-01 15:32:23 -05:00
AnthoPak c96df6d35c Allow changing UIWindow's animation speed (#629) 2022-09-26 17:20:15 -05:00
Tanner Bennett fff3059099 Use keyboard WILL show events 🚀🚀🚀 2022-09-21 19:32:30 -05:00
Tanner Bennett 4bee6d17bc Silence some totally valid availability warnings 2022-09-21 19:26:43 -05:00
Tanner Bennett 3d8a12027b Ignore SPM build folder 2022-09-21 19:26:43 -05:00
Natheer bc96542856 Move FLEX out of the way of the keyboard (#623) 2022-09-21 19:02:01 -05:00
Jerry.Qiushi 24526a6a0a Podspec: update xcconfig to pod_target_xcconfig (#626) 2022-09-21 17:20:04 -05:00
Pierre-Yves B d33d909fa0 Add missing NSDateFormatter extension to SPM headers (#616) 2022-08-31 15:29:41 -05:00
Chaoshuai Lü 0a9db69419 Use queue instead of self.queue in FLEXNetworkRecorder.m 2022-08-30 11:58:56 -07:00
Tanner Bennett 2db78c3bef APNS: hook correct UNUNCenterDelegate method 2022-08-27 16:37:04 -05:00
Sergo Beruashvili 1b91bc963e Call missing websocket completion (#609)
Co-authored-by: Sergo Beruashvili <sergo.beruashvili@daimler.com>
2022-08-10 02:45:23 -07:00
Enrique Garcia 56d90748cb Fix Xcode 14 SPM warning related to iOS 11 support (#610) 2022-07-31 15:25:59 -05:00
Tanner Bennett 9997721e0a Display the device token as bytes, and tap to copy 2022-07-25 17:45:06 -05:00
Tanner Bennett bedbd0b8c8 FLEXAlert: shortcut to present a temporary alert 2022-07-25 17:44:50 -05:00
Tanner Bennett cb64c88c59 Polish notification hooks
They'll crash if the method is unimplemented; I forgot these methods are all optional
2022-07-25 17:28:16 -05:00
Tanner Bennett be733c30cb Use summary instead of description for remote notifs 2022-07-25 01:51:33 -05:00
Tanner Bennett e46d26034b Flatten intendations in descriptions 2022-07-25 01:51:19 -05:00
Tanner Bennett 3809ece1f9 Distinguish between remote and user notifications
Also change how we get app delegate class
2022-07-24 23:55:52 -05:00
Tanner Bennett b2410750fd Wrap NSDateFormatter 2022-07-24 23:55:52 -05:00
Tanner Bennett 72ccd007b3 APNS: hook Swift UIApplicationMain shim as well 2022-07-24 23:55:52 -05:00
helloworld 4375f1a67f Fix iOS 16 _statusBarWindow crash (#607)
Co-authored-by: ClownFish <15800960640@163.com>
2022-07-05 22:56:00 -05:00
Tanner Bennett 393a43754c Misc 2022-07-02 02:20:26 -05:00
Tanner Bennett b56d6f6d33 Add FLEXAPNSViewController for viewing push notifs 2022-07-02 02:20:26 -05:00
Tanner Bennett 0d985b504b Add auto keyword 2022-07-02 02:20:26 -05:00
Tanner Bennett 148890914d Add nullability specifiers to FLEXSingleRowSection 2022-06-27 18:39:49 -05:00
Tanner Bennett c734ad16e5 Just a lil something for the demo app 2022-06-27 18:39:25 -05:00
Tanner Bennett ac6301437b SPM project: _MSSafeMode env var 2022-06-27 18:39:07 -05:00
Tanner Bennett b45c655ba2 Hopefully fix #574 2022-06-27 18:38:05 -05:00
Tanner Bennett 83fe11210e Fix crash in FLEXFirebaseSetDataInfo 2022-06-12 01:26:42 -05:00
Tanner Bennett e80e3bb5b7 Add API to push explorer on any UINavigationController 2022-05-26 21:31:49 -05:00
Tanner Bennett 833b8e7534 Add APIs to programmatically present …
- an object explorer
- an arbitrary view controller

from FLEXManager
2022-05-26 14:45:18 -05:00
Tanner Bennett 56e8693c41 Add NSPhotoLibraryUsageDescription to example 2022-05-26 14:20:05 -05:00
Tanner Bennett 49eeb28464 Don't load tweaks when debugging the example app 2022-05-26 14:19:55 -05:00
Tanner Bennett a24d752313 Fix UIActivityViewController dismissing presenter 2022-05-26 14:19:35 -05:00
Tanner Bennett 643523a779 Add FLEXActivityViewController 2022-05-26 14:19:15 -05:00
Tanner Bennett 65744675db Fix tabs not being closed in some instances 2022-05-26 12:53:29 -05:00
Chaoshuai Lü f994346919 Match Availability versions for FLEXTableViewSection 2022-05-12 13:46:05 -07:00
Chaoshuai Lü a7a8b8e33e Fix warnings
* Fix -Wobjc-signed-char-bool-implicit-int-conversion in FLEXSwiftInternal
* Fix -Wparentheses
2022-05-11 23:19:37 -05:00
Tanner Bennett f254720408 Display dlopen errors 2022-04-27 23:45:32 -05:00
Tanner Bennett e6fd5ed24e Fix bug in new FLEXMirror usage 2022-04-27 13:45:42 -05:00
Tanner Bennett d13f263492 Update SPM example deps 2022-04-27 13:45:42 -05:00
Tanner Bennett ba36647398 Enable displaying ivar names for custom struct types
FLEXMetadataExtras.h
2022-04-27 13:45:42 -05:00
Tanner Bennett 9d97533649 Make CommitDetails a struct to demo Reflex 2022-04-27 13:45:42 -05:00
Tanner Bennett 96b13e66b6 Podspec: gnu++11 2022-04-27 13:45:42 -05:00
Tanner Bennett 9712a4c869 Delete modulemap 2022-04-27 13:45:42 -05:00
Tanner Bennett 8f9956308f Use Reflex instead of FLEX in SPM project 2022-04-27 13:45:42 -05:00
Tanner Bennett 69e88acbee Initialize FLEXObjectExplorer.reflexAvailable 2022-04-27 13:45:42 -05:00
Tanner Bennett 07587986df Use Reflex and FLEXMirror 2022-04-27 13:45:42 -05:00
Tanner Bennett e6641a5f9d Add FLEXSwiftInternal to check if object from Swift 2022-04-27 13:45:42 -05:00
Tanner Bennett 111fb6c9e6 Modernize FLEXMirror
Differentiate between class and instance props/methods
2022-04-27 13:45:42 -05:00
Tanner Bennett 995e7eadbe Add (unused) libflex modulemap file
For my own use case, I dynamically link FLEX, so I need to build Reflex using libflex.tbd as well as this modulemap file.
2022-04-27 13:45:42 -05:00
Tanner Bennett ac50a6d36b Bump version 2022-04-27 13:45:41 -05:00
Tanner Bennett af40ce0909 Fix annoying divider color in editor screens 2022-04-27 13:43:40 -05:00
matrush fb2a33876e Add FLEXTableRowDataViewController for viewing DB rows 2022-04-24 16:30:43 -07:00
Tanner Bennett a42efe17a1 Use host instead of self here 2022-04-24 16:30:43 -07:00
Tanner Bennett 4bc2d1c7a9 Shorten some line lengths 2022-04-24 14:48:47 -07:00
weiminghuaa c7ebecfcb3 Re-filter system log when new messages arrive 2022-04-24 14:48:47 -07:00
weiminghuaa 208f0a31e4 Pin seach bar in system log 2022-04-24 14:48:47 -07:00
Tanner Bennett 2aeb34a67e Cherry pick #592 2022-04-24 16:23:15 -05:00
Tanner Bennett 39f16bd039 Make properties atomic in hopes of resolving #574 2022-04-24 16:23:00 -05:00
Tanner Bennett eea681d6c5 Consolidate duplicate code 2022-04-24 16:22:08 -05:00
Tanner Bennett a8768da4b9 Fix crash where FIRDocumentSnapshot response is nil 2022-04-24 16:15:34 -05:00
Tanner Bennett 4f6fd29d38 Clean up FLEXMITMDataSource interface 2022-04-23 18:45:24 -05:00
Canius Chu ba9cb43b6a Fix compile errors and warnings (#597)
* Update INSTALL_PATH
* Update run path
* Add FLEXFirebaseTransaction.mm into project which break in commit: d2e0db8773
* Fix header imports
* Rollback LD_RUNPATH_SEARCH_PATHS
2022-04-23 18:12:17 -05:00
Tanner Bennett 3af6da8554 Format _manuallyDeactivateSearchOnDisappear 2022-04-23 18:12:17 -05:00
Chaoshuai Lü 6c21395ac8 Fix self.allTransactions race condition in FLEXMITMDataSource
There is a crash caused by this because of race condition. Here we can simply make a copy of the transaction and it will be used and released properly even within the block.
2022-03-30 20:55:19 -07:00
Chaoshuai Lü 456fda31cd Add fallback column query support when PRAGMA table_info is not working (#573)
* Add fallback column queries support when PRAGMA table_info is not working

* Switch to check columnCount instead of rowsAffected

* Use SELECT * FROM pragma_table_info('%@')

Fallback to SELECT * FROM table where 0=1

Co-authored-by: Tanner Bennett <tannerbennett@me.com>
2022-03-30 20:23:55 -07:00
Chaoshuai Lü 9fc8735925 Check rowIDs instead of rowIDs.count to validate input 2022-03-30 23:16:07 -04:00
Tanner Bennett 9683acbe4f Ignore workspace files 2022-03-28 20:55:31 -07:00
Tanner Bennett 86a1cc9324 Remove DEVELOPMENT_TEAM 2022-03-28 20:55:15 -07:00
Tanner Bennett 88b14b3a92 Escape text for webview on a background thread 2022-03-28 20:54:34 -07:00
Tanner Bennett b9cd2682a7 Clean up web vc file 2022-03-28 20:43:48 -07:00
Nick Holub 64d1534fae Add accessibilityIdentifier and accessibilityLabel to views properties 2022-03-22 15:53:57 -07:00
Chaoshuai Lü 8318902826 Fix -Wobjc-signed-char-bool-implicit-int-conversion issue in FLEXNavigationController
This should be checked explicitly to shut up the warning.
2022-03-08 03:16:34 -08:00
Chaoshuai Lu a7f41fe5fc Mark some firebase related functions as static to avoid warnings 2022-03-03 03:14:38 -06:00
Tanner Bennett 2983649cdb Show descriptions of complex FIRQuery objects 2021-12-25 03:27:58 -06:00
Tanner Bennett d2e0db8773 Move FLEXFirebaseTransaction to own file 2021-12-24 22:03:55 -06:00
Tanner Bennett fd98070166 Fix #499, thank you @zhaogyrain 2021-12-24 19:41:52 -06:00
Hossam Ghareeb 4a9fd00eda Add option to copy row as CSV
This will make it easier to insert a new row
2021-12-24 19:35:53 -06:00
Tanner Bennett 97205fc808 Fix bug in rowid selection
`SELECT rowid` returns rowids corresponding to rows in a different order than `SELECT *`. We will now order the rowids in ascending order to match the order of `SELECT *`.

Previously, the mismatched order would mean deleting a row in the UI might delete a different row.
2021-12-24 19:32:01 -06:00
Tanner Bennett 0cffe72a8f Add more assertions to FLEXTableContentVC 2021-12-24 19:32:01 -06:00
Tanner Bennett d5ab2ee916 Revise copy for FLEXTableContentVC add row screen 2021-12-24 19:32:01 -06:00
Tanner Bennett 90c9a48694 Only allow deleting rows if we have a table name 2021-12-24 19:32:01 -06:00
Tanner Bennett 0222c682a0 Improve FLEXTableContentVC toolbar button logic
Only allow adding rows or deleting rows if we have a table name
2021-12-24 19:32:01 -06:00
Tanner Bennett 1e6f6ee110 Clean up FLEXTableContentVC initializers 2021-12-24 19:32:01 -06:00
Hossam Ghareeb 00d6b348f0 Adding an option to add a row in DB table
Added a new toolbar button to insert a new row. Tapping on the button will show an alert to write the values as a comma separated string which will be added in an INSERT statement to be executed.
2021-12-24 19:32:01 -06:00
Tanner Bennett 5404f37d0f Remember the last selected network observer tab 2021-12-24 19:30:18 -06:00
Tanner Bennett 1e539c7129 Pin network history search bar
This works around an ugly visual glitch that happens when the segmented control is visible
2021-12-24 19:30:18 -06:00
Tanner Bennett ca1b202949 For now, just show the transaction object directly 2021-12-24 19:30:18 -06:00
Tanner Bennett 62220a9a65 Record Firebase document creation 2021-12-24 19:30:18 -06:00
Tanner Bennett 02730c6c86 Allow clearing only filtered requests 2021-12-24 19:30:18 -06:00
Tanner Bennett 68789fbe1d Allow filtering Firebase requests with push/pull 2021-12-24 19:30:18 -06:00
Tanner Bennett b4261f8647 Add description for Firebase transaction "push" type 2021-12-24 19:30:18 -06:00
Tanner Bennett 94bf4eac2a Record "push" Firebase network transaction types 2021-12-23 01:35:17 -06:00
Tanner Bennett bed52392e5 Add new Firebase network transaction types 2021-12-23 01:35:17 -06:00
Tanner Bennett c4891840bd Wrap some method names 2021-12-23 01:35:17 -06:00
Tanner Bennett 9720227ac7 Fix logic in network observer mode property 2021-12-23 01:35:17 -06:00
Tanner Bennett 74622aaf10 Initial work for Firebase 2021-12-23 01:35:17 -06:00
Tanner Bennett 0cb5ad8453 Restore -Wno-deprecated-declarations 2021-12-23 01:35:09 -06:00
Chaoshuai Lu def68eae48 Remove unneeded edgesForExtendedLayout setting
Update FLEXTableContentViewController.m

Remove empty lines/spaces changes
2021-12-05 15:29:30 -08:00
118 changed files with 3161 additions and 513 deletions
+2
View File
@@ -20,3 +20,5 @@ DerivedData
/Example/Pods
Podfile.lock
IDEWorkspaceChecks.plist
*.xcworkspace
.build
@@ -16,4 +16,13 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface UINavigationController (FLEXObjectExploring)
/// Push an object explorer view controller onto the navigation stack
- (void)pushExplorerForObject:(id)object;
/// Push an object explorer view controller onto the navigation stack
- (void)pushExplorerForObject:(id)object animated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END
@@ -8,6 +8,7 @@
#import "FLEXNavigationController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXTabList.h"
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
@@ -28,7 +29,8 @@
@implementation FLEXNavigationController
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
return [[self alloc] initWithRootViewController:rootVC];
FLEXNavigationController *nav = [[self alloc] initWithRootViewController:rootVC];
return nav;
}
- (void)viewDidLoad {
@@ -65,6 +67,17 @@
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (@available(iOS 15.0, *)) {
UISheetPresentationController *presenter = self.sheetPresentationController;
presenter.detents = @[
UISheetPresentationControllerDetent.mediumDetent,
UISheetPresentationControllerDetent.largeDetent,
];
presenter.prefersScrollingExpandsWhenScrolledToEdge = NO;
presenter.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
presenter.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
}
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
for (UIViewController *vc in self.viewControllers) {
[self addNavigationBarItemsToViewController:vc.navigationItem];
@@ -93,6 +106,13 @@
[self addNavigationBarItemsToViewController:viewController.navigationItem];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
// Workaround for UIActivityViewController trying to dismiss us for some reason
if (![self.viewControllers.lastObject.presentedViewController isKindOfClass:UIActivityViewController.self]) {
[super dismissViewControllerAnimated:flag completion:completion];
}
}
- (void)dismissAnimated {
// Tabs are only closed if the done button is pressed; this
// allows you to leave a tab open by dragging down to dismiss
@@ -104,7 +124,7 @@
}
- (BOOL)canShowToolbar {
return self.topViewController.toolbarItems.count;
return self.topViewController.toolbarItems.count > 0;
}
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
@@ -194,3 +214,18 @@
}
@end
@implementation UINavigationController (FLEXObjectExploring)
- (void)pushExplorerForObject:(id)object {
[self pushExplorerForObject:object animated:YES];
}
- (void)pushExplorerForObject:(id)object animated:(BOOL)animated {
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
if (explorer) {
[self pushViewController:explorer animated:animated];
}
}
@end
@@ -66,9 +66,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
_style = style;
_manuallyDeactivateSearchOnDisappear = ({
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
});
_manuallyDeactivateSearchOnDisappear = (
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11
);
// We will be our own search delegate if we implement this method
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
@@ -96,7 +96,11 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
if (@available(iOS 9.1, *)) {
self.searchController.obscuresBackgroundDuringPresentation = NO;
} else {
self.searchController.dimsBackgroundDuringPresentation = NO;
}
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
@@ -210,6 +214,8 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.estimatedRowHeight = 10;
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
_bookmarksToolbarItem = [UIBarButtonItem
flex_itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
+8 -4
View File
@@ -8,6 +8,8 @@
#import "FLEXTableViewSection.h"
NS_ASSUME_NONNULL_BEGIN
/// A section providing a specific single row.
///
/// You may optionally provide a view controller to push when the row
@@ -16,13 +18,15 @@
@interface FLEXSingleRowSection : FLEXTableViewSection
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
+ (instancetype)title:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
+ (instancetype)title:(nullable NSString *)sectionTitle
reuse:(nullable NSString *)reuseIdentifier
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
@property (nonatomic) UIViewController *pushOnSelection;
@property (nonatomic) void (^selectionAction)(UIViewController *host);
@property (nullable, nonatomic) UIViewController *pushOnSelection;
@property (nullable, nonatomic) void (^selectionAction)(UIViewController *host);
/// Called to determine whether the single row should display itself or not.
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
@end
NS_ASSUME_NONNULL_END
+2
View File
@@ -30,6 +30,8 @@
- (id)initWithTitle:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
NSParameterAssert(cellConfiguration);
self = [super init];
if (self) {
_title = sectionTitle;
+1 -1
View File
@@ -79,7 +79,7 @@
return @"";
}
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) {
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0)) {
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
@@ -46,7 +46,13 @@
UIPickerView *fontsPicker = [UIPickerView new];
fontsPicker.dataSource = self;
fontsPicker.delegate = self;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// Deprecated in iOS 13; from then on, selection is always shown
fontsPicker.showsSelectionIndicator = YES;
#pragma clang diagnostic pop
return fontsPicker;
}
@@ -10,4 +10,7 @@
@interface FLEXArgumentInputStructView : FLEXArgumentInputView
/// Enable displaying ivar names for custom struct types
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
@end
@@ -19,6 +19,41 @@
@implementation FLEXArgumentInputStructView
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *structFieldNameRegistrar = nil;
+ (void)initialize {
if (self == [FLEXArgumentInputStructView class]) {
structFieldNameRegistrar = [NSMutableDictionary new];
[self registerDefaultFieldNames];
}
}
+ (void)registerDefaultFieldNames {
NSDictionary *defaults = @{
@(@encode(CGRect)): @[@"CGPoint origin", @"CGSize size"],
@(@encode(CGPoint)): @[@"CGFloat x", @"CGFloat y"],
@(@encode(CGSize)): @[@"CGFloat width", @"CGFloat height"],
@(@encode(CGVector)): @[@"CGFloat dx", @"CGFloat dy"],
@(@encode(UIEdgeInsets)): @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"],
@(@encode(UIOffset)): @[@"CGFloat horizontal", @"CGFloat vertical"],
@(@encode(NSRange)): @[@"NSUInteger location", @"NSUInteger length"],
@(@encode(CATransform3D)): @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"],
@(@encode(CGAffineTransform)): @[@"CGFloat a", @"CGFloat b",
@"CGFloat c", @"CGFloat d",
@"CGFloat tx", @"CGFloat ty"],
};
[structFieldNameRegistrar addEntriesFromDictionary:defaults];
if (@available(iOS 11.0, *)) {
structFieldNameRegistrar[@(@encode(NSDirectionalEdgeInsets))] = @[
@"CGFloat top", @"CGFloat leading", @"CGFloat bottom", @"CGFloat trailing"
];
}
}
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
@@ -181,40 +216,13 @@
return NO;
}
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
NSParameterAssert(typeEncoding); NSParameterAssert(names);
structFieldNameRegistrar[typeEncoding] = names;
}
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
NSArray<NSString *> *customTitles = nil;
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
customTitles = @[@"CGPoint origin", @"CGSize size"];
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
customTitles = @[@"CGFloat x", @"CGFloat y"];
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
customTitles = @[@"CGFloat width", @"CGFloat height"];
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
customTitles = @[@"CGFloat horizontal", @"CGFloat vertical"];
} else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
customTitles = @[@"NSUInteger location", @"NSUInteger length"];
} else if (strcmp(typeEncoding, @encode(CATransform3D)) == 0) {
customTitles = @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"];
} else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
customTitles = @[@"CGFloat a", @"CGFloat b",
@"CGFloat c", @"CGFloat d",
@"CGFloat tx", @"CGFloat ty"];
} else {
if (@available(iOS 11.0, *)) {
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat leading",
@"CGFloat bottom", @"CGFloat trailing"];
}
}
}
return customTitles;
return structFieldNameRegistrar[@(typeEncoding)];
}
@end
@@ -21,4 +21,7 @@
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
/// Enable displaying ivar names for custom struct types
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
@end
@@ -67,4 +67,9 @@
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
}
/// Enable displaying ivar names for custom struct types
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
[FLEXArgumentInputStructView registerFieldNames:names forTypeEncoding:typeEncoding];
}
@end
+2 -1
View File
@@ -9,6 +9,7 @@
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
@interface FLEXFieldEditorView ()
@@ -122,7 +123,7 @@
}
+ (UIColor *)dividerColor {
return UIColor.lightGrayColor;
return FLEXColor.tertiaryBackgroundColor;
}
+ (CGFloat)horizontalPadding {
@@ -11,12 +11,14 @@
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXMetadataExtras.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, readonly) id<FLEXMetadataAuxiliaryInfo> auxiliaryInfoProvider;
@property (nonatomic) FLEXProperty *property;
@property (nonatomic) FLEXIvar *ivar;
@@ -30,14 +32,14 @@
#pragma mark - Initialization
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit {
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^)(void))onCommit {
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
editor.title = [@"Property: " stringByAppendingString:property.name];
editor.property = property;
return editor;
}
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit {
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^)(void))onCommit {
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
editor.ivar = ivar;
@@ -61,6 +63,8 @@
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
];
[self registerAuxiliaryInfo];
// Configure input view
self.fieldEditorView.fieldDescription = self.fieldDescription;
@@ -122,6 +126,17 @@
#pragma mark - Private
- (void)registerAuxiliaryInfo {
// This is how Reflex will get Swift struct field names into the editor at runtime
NSDictionary<NSString *, NSArray *> *labels = [self.auxiliaryInfoProvider
auxiliaryInfoForKey:FLEXAuxiliarynfoKeyFieldLabels
];
for (NSString *type in labels) {
[FLEXArgumentInputViewFactory registerFieldNames:labels[type] forTypeEncoding:type];
}
}
- (id)currentValue {
if (self.property) {
return [self.property getValue:self.target];
@@ -130,6 +145,10 @@
}
}
- (id<FLEXMetadataAuxiliaryInfo>)auxiliaryInfoProvider {
return self.ivar ?: self.property;
}
- (const FLEXTypeEncoding *)typeEncoding {
if (self.property) {
return self.property.attributes.typeEncoding.UTF8String;
@@ -37,7 +37,7 @@
_commitHandler = onCommit;
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification object:nil
name:UIKeyboardWillShowNotification object:nil
];
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardWillHide:)
@@ -129,6 +129,14 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (@available(iOS 10.0, *)) {
_selectionFBG = [UISelectionFeedbackGenerator new];
}
// Observe keyboard to move self out of the way
[NSNotificationCenter.defaultCenter
addObserver:self
selector:@selector(keyboardShown:)
name:UIKeyboardWillShowNotification
object:nil
];
}
- (void)viewWillAppear:(BOOL)animated {
@@ -162,7 +170,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
// We check its class by name because using isKindOfClass will fail for the same class defined
// twice in the runtime; and the goal here is to avoid calling -supportedInterfaceOrientations
// recursively when I'm inspecting FLEX with itself from a tweak dylib
if (viewControllerToAsk && ![NSStringFromClass([viewControllerToAsk class]) hasPrefix:@"FLEX"]) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
@@ -375,6 +386,21 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return [self.view convertRect:frameInWindow fromView:nil];
}
- (void)keyboardShown:(NSNotification *)notif {
CGRect keyboardFrame = [notif.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect toolbarFrame = self.explorerToolbar.frame;
if (CGRectGetMinY(keyboardFrame) < CGRectGetMaxY(toolbarFrame)) {
toolbarFrame.origin.y = keyboardFrame.origin.y - toolbarFrame.size.height;
// Subtract a little more, to ignore accessory input views
toolbarFrame.origin.y -= 50;
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:1 initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveEaseOut animations:^{
[self updateToolbarPositionWithUnconstrainedFrame:toolbarFrame];
} completion:nil];
}
}
#pragma mark - Toolbar Buttons
@@ -403,8 +429,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
- (UIWindow *)statusWindow {
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [UIApplication.sharedApplication valueForKey:statusBarString];
if (!@available(iOS 16, *)) {
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
return nil;
}
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
@@ -436,7 +466,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
// Recent only enabled when we have a last active tab
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
if (!self.presentedViewController) {
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
} else {
toolbar.recentItem.enabled = NO;
}
}
@@ -818,31 +852,34 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
BOOL shouldReceiveTouch = NO;
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
// Always if it's on the toolbar
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
shouldReceiveTouch = YES;
// If we have a modal presented, is it in the modal?
if (self.presentedViewController) {
UIView *presentedView = self.presentedViewController.view;
CGPoint pipvc = [presentedView convertPoint:pointInLocalCoordinates fromView:self.view];
UIView *hit = [presentedView hitTest:pipvc withEvent:nil];
if (hit != nil) {
return YES;
}
}
// Always if we're in selection mode
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
shouldReceiveTouch = YES;
if (self.currentMode == FLEXExplorerModeSelect) {
return YES;
}
// Always in move mode too
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
shouldReceiveTouch = YES;
if (self.currentMode == FLEXExplorerModeMove) {
return YES;
}
// Always if we have a modal presented
if (!shouldReceiveTouch && self.presentedViewController) {
shouldReceiveTouch = YES;
// Always if it's on the toolbar
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
return YES;
}
return shouldReceiveTouch;
return NO;
}
@@ -888,8 +925,14 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// up in case we start replacing them again in the future
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
[self updateButtonStates];
// Show the view controller
[super presentViewController:toPresent animated:animated completion:completion];
[super presentViewController:toPresent animated:animated completion:^{
[self updateButtonStates];
if (completion) completion();
}];
}
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
@@ -910,7 +953,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self updateButtonStates];
[super dismissViewControllerAnimated:animated completion:completion];
[super dismissViewControllerAnimated:animated completion:^{
[self updateButtonStates];
if (completion) completion();
}];
}
- (BOOL)wantsWindowToBecomeKey {
+2 -7
View File
@@ -18,18 +18,13 @@
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
// UIWindowLevelStatusBar + 100 seems to hit that balance.
self.windowLevel = UIWindowLevelStatusBar + 100.0;
self.windowLevel = UIWindowLevelAlert - 1;
}
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
}
return pointInside;
return [self.eventDelegate shouldHandleTouchAtPoint:point];
}
- (BOOL)shouldAffectStatusBarAppearance {
@@ -84,6 +84,16 @@
if (idx != NSNotFound) {
[self closeTabAtIndex:idx];
}
// Not sure how this is possible, but it happens sometimes
if (self.activeTab == tab) {
[self chooseNewActiveTab];
}
// It is possible for an object explorer to form a retain cycle
// with its own navigation controller; clearing the view controllers
// manually when closing a tab breaks the cycle
tab.viewControllers = @[];
}
- (void)closeTabAtIndex:(NSInteger)idx {
+12 -13
View File
@@ -6,17 +6,16 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "UIBarButtonItem+FLEX.h"
#import "CALayer+FLEX.h"
#import "UIFont+FLEX.h"
#import "UIGestureRecognizer+Blocks.h"
#import "UIPasteboard+FLEX.h"
#import "UIMenu+FLEX.h"
#import "UITextField+Range.h"
#import <UIBarButtonItem+FLEX.h>
#import <CALayer+FLEX.h>
#import <UIFont+FLEX.h>
#import <UIGestureRecognizer+Blocks.h>
#import <UIPasteboard+FLEX.h>
#import <UIMenu+FLEX.h>
#import <UITextField+Range.h>
#import <NSObject+FLEX_Reflection.h>
#import <NSArray+FLEX.h>
#import <NSUserDefaults+FLEX.h>
#import <NSTimer+FLEX.h>
#import "NSObject+FLEX_Reflection.h"
#import "NSArray+FLEX.h"
#import "NSUserDefaults+FLEX.h"
#import "NSTimer+FLEX.h"
#import "NSDateFormatter+FLEX.h"
+11 -11
View File
@@ -6,17 +6,17 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEXFilteringTableViewController.h>
#import <FLEXNavigationController.h>
#import <FLEXTableViewController.h>
#import <FLEXTableView.h>
#import "FLEXFilteringTableViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXTableViewController.h"
#import "FLEXTableView.h"
#import <FLEXSingleRowSection.h>
#import <FLEXTableViewSection.h>
#import "FLEXSingleRowSection.h"
#import "FLEXTableViewSection.h"
#import <FLEXCodeFontCell.h>
#import <FLEXSubtitleTableViewCell.h>
#import <FLEXTableViewCell.h>
#import <FLEXMultilineTableViewCell.h>
#import <FLEXKeyValueTableViewCell.h>
#import "FLEXCodeFontCell.h"
#import "FLEXSubtitleTableViewCell.h"
#import "FLEXTableViewCell.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXKeyValueTableViewCell.h"
+11 -11
View File
@@ -6,17 +6,17 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEXObjectExplorerFactory.h>
#import <FLEXObjectExplorerViewController.h>
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import <FLEXObjectExplorer.h>
#import "FLEXObjectExplorer.h"
#import <FLEXShortcut.h>
#import <FLEXShortcutsSection.h>
#import "FLEXShortcut.h"
#import "FLEXShortcutsSection.h"
#import <FLEXCollectionContentSection.h>
#import <FLEXColorPreviewSection.h>
#import <FLEXDefaultsContentSection.h>
#import <FLEXMetadataSection.h>
#import <FLEXMutableListSection.h>
#import <FLEXObjectInfoSection.h>
#import "FLEXCollectionContentSection.h"
#import "FLEXColorPreviewSection.h"
#import "FLEXDefaultsContentSection.h"
#import "FLEXMetadataSection.h"
#import "FLEXMutableListSection.h"
#import "FLEXObjectInfoSection.h"
+17 -16
View File
@@ -6,21 +6,22 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEXObjcInternal.h>
#import <FLEXSwiftInternal.h>
#import <FLEXRuntimeSafety.h>
#import <FLEXBlockDescription.h>
#import <FLEXTypeEncodingParser.h>
#import "FLEXObjcInternal.h"
#import "FLEXSwiftInternal.h"
#import "FLEXRuntimeSafety.h"
#import "FLEXBlockDescription.h"
#import "FLEXTypeEncodingParser.h"
#import <FLEXMirror.h>
#import <FLEXProtocol.h>
#import <FLEXProperty.h>
#import <FLEXIvar.h>
#import <FLEXMethodBase.h>
#import <FLEXMethod.h>
#import <FLEXPropertyAttributes.h>
#import <FLEXRuntime+Compare.h>
#import <FLEXRuntime+UIKitHelpers.h>
#import "FLEXMirror.h"
#import "FLEXProtocol.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
#import "FLEXMethodBase.h"
#import "FLEXMethod.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXRuntime+Compare.h"
#import "FLEXRuntime+UIKitHelpers.h"
#import "FLEXMetadataExtras.h"
#import <FLEXProtocolBuilder.h>
#import <FLEXClassBuilder.h>
#import "FLEXProtocolBuilder.h"
#import "FLEXClassBuilder.h"
+13 -13
View File
@@ -7,19 +7,19 @@
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <FLEXManager.h>
#import <FLEXManager+Extensibility.h>
#import <FLEXManager+Networking.h>
#import "FLEXManager.h"
#import "FLEXManager+Extensibility.h"
#import "FLEXManager+Networking.h"
#import <FLEXExplorerToolbar.h>
#import <FLEXExplorerToolbarItem.h>
#import <FLEXGlobalsEntry.h>
#import "FLEXExplorerToolbar.h"
#import "FLEXExplorerToolbarItem.h"
#import "FLEXGlobalsEntry.h"
#import <FLEX-Core.h>
#import <FLEX-Runtime.h>
#import <FLEX-Categories.h>
#import <FLEX-ObjectExploring.h>
#import "FLEX-Core.h"
#import "FLEX-Runtime.h"
#import "FLEX-Categories.h"
#import "FLEX-ObjectExploring.h"
#import <FLEXMacros.h>
#import <FLEXAlert.h>
#import <FLEXResources.h>
#import "FLEXMacros.h"
#import "FLEXAlert.h"
#import "FLEXResources.h"
@@ -13,7 +13,7 @@
@synthesize keyedRows = _keyedRows;
+ (instancetype)message:(NSString *)message {
return [[self alloc] initWithmessage:message columns:nil rows:nil];
return [[self alloc] initWithMessage:message columns:nil rows:nil];
}
+ (instancetype)error:(NSString *)message {
@@ -23,12 +23,12 @@
}
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData];
}
- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
- (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows {
NSParameterAssert(message || (columns && rows));
NSParameterAssert(columns.count == rows.firstObject.count);
NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count);
self = [super init];
if (self) {
@@ -12,7 +12,10 @@
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
#define kQuery(name, str) static NSString * const QUERY_##name = str
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic) sqlite3 *db;
@@ -107,7 +110,19 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
FLEXSQLResult *results = [self executeStatement:sql];
// https://github.com/FLEXTool/FLEX/issues/554
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
results = [self executeStatement:sql];
// Fallback to empty query
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
return [self executeStatement:sql].columns ?: @[];
}
}
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}] ?: @[];
@@ -119,7 +134,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT rowid FROM \"%@\"", tableName];
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
@@ -146,7 +161,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
return self.lastResult;
}
// Grab columns
// Grab columns (columnCount will be 0 for insert/update/delete)
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return @(sqlite3_column_name(pstmt, (int)i));
@@ -164,8 +179,9 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
if (status == SQLITE_DONE) {
if (rows.count) {
// We selected some rows
// columnCount will be 0 for insert/update/delete
if (rows.count || columnCount > 0) {
// We executed a SELECT query
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
} else {
// We executed a query like INSERT, UDPATE, or DELETE
@@ -13,13 +13,23 @@ NS_ASSUME_NONNULL_BEGIN
@interface FLEXTableContentViewController : UIViewController
/// Display a table with the given columns, rows, and name.
/// Display a mutable table with the given columns, rows, and name.
///
/// @param columnNames self explanatory.
/// @param rowData an array of rows, where each row is an array of column data.
/// @param rowIDs an array of stringy row IDs. Required for deleting rows.
/// @param tableName an optional name of the table being viewed, if any. Enables adding rows.
/// @param databaseManager an optional manager to allow modifying the table.
/// Required for deleting rows. Required for adding rows if \c tableName is supplied.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIds
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager;
database:(id<FLEXDatabaseManager>)databaseManager;
/// Display an immutable table with the given columns and rows.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@end
@@ -7,6 +7,7 @@
//
#import "FLEXTableContentViewController.h"
#import "FLEXTableRowDataViewController.h"
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
@@ -21,6 +22,8 @@
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
@property (nonatomic, readonly) BOOL canRefresh;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@@ -28,16 +31,43 @@
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
FLEXTableContentViewController *controller = [self new];
controller->_columns = columnNames.copy;
controller->_rows = rowData.mutableCopy;
controller->_rowIDs = rowIDs.mutableCopy;
controller->_tableName = tableName.copy;
controller->_databaseManager = databaseManager;
return controller;
database:(id<FLEXDatabaseManager>)databaseManager {
return [[self alloc]
initWithColumns:columnNames
rows:rowData
rowIDs:rowIDs
tableName:tableName
database:databaseManager
];
}
+ (instancetype)columns:(NSArray<NSString *> *)cols
rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil];
}
- (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
tableName:(nullable NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
// Must supply all optional parameters as one, or none
BOOL all = rowIDs && tableName && databaseManager;
BOOL none = !rowIDs && !tableName && !databaseManager;
NSParameterAssert(all || none);
self = [super init];
if (self) {
self->_columns = columnNames.copy;
self->_rows = rowData.mutableCopy;
self->_rowIDs = rowIDs.mutableCopy;
self->_tableName = tableName.copy;
self->_databaseManager = databaseManager;
}
return self;
}
- (void)loadView {
@@ -49,7 +79,6 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.title = self.tableName;
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.multiColumnView reloadData];
[self setupToolbarItems];
}
@@ -67,6 +96,10 @@
return _multiColumnView;
}
- (BOOL)canRefresh {
return self.databaseManager && self.tableName;
}
#pragma mark MultiColumnTableView DataSource
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
@@ -122,6 +155,10 @@
return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field];
}];
NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) {
return [NSString stringWithFormat:@"'%@'", value];
}];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
NSString *message = [fields componentsJoinedByString:@"\n\n"];
@@ -129,10 +166,19 @@
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "];
});
make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) {
UIViewController *focusedRow = [FLEXTableRowDataViewController
rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns]
];
[self.navigationController pushViewController:focusedRow animated:YES];
});
// Option to delete row
BOOL hasRowID = self.rows.count && row < self.rows.count;
if (hasRowID) {
if (hasRowID && self.canRefresh) {
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteRow = [NSString stringWithFormat:
@"DELETE FROM %@ WHERE rowid = %@",
@@ -142,9 +188,7 @@
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
// Remove deleted row and reload view
if (success) {
[self.rowIDs removeObjectAtIndex:row];
[self.rows removeObjectAtIndex:row];
[self.multiColumnView reloadData];
[self reloadTableDataFromDB];
}
}];
});
@@ -216,14 +260,24 @@
return;
}
UIBarButtonItem *trashButton = [FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed))
flex_withTintColor:UIColor.redColor
UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed));
UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed));
// Only allow adding rows or deleting rows if we have a table name
trashButton.enabled = self.canRefresh;
addButton.enabled = self.canRefresh;
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
addButton,
UIBarButtonItem.flex_flexibleSpace,
[trashButton flex_withTintColor:UIColor.redColor],
];
trashButton.enabled = self.databaseManager && self.rows.count;
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, trashButton];
}
- (void)trashPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Delete All Rows");
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
@@ -241,9 +295,35 @@
} showFrom:self];
}
- (void)addPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add a New Row");
make.message(@"Comma separate values to use in an INSERT statement.\n\n");
make.message(@"INSERT INTO [table] VALUES (your_input)");
make.textField(@"5, 'John Smith', 14,...");
make.button(@"Insert").handler(^(NSArray<NSString *> *strings) {
NSString *statement = [NSString stringWithFormat:
@"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0]
];
[self executeStatementAndShowResult:statement completion:^(BOOL success) {
if (success) {
[self reloadTableDataFromDB];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
#pragma mark - Helpers
- (void)executeStatementAndShowResult:(NSString *)statement completion:(void (^_Nullable)(BOOL success))completion {
- (void)executeStatementAndShowResult:(NSString *)statement
completion:(void (^_Nullable)(BOOL success))completion {
NSParameterAssert(self.databaseManager);
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
@@ -260,5 +340,20 @@
} showFrom:self];
}
- (void)reloadTableDataFromDB {
if (!self.canRefresh) {
return;
}
NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName];
NSArray<NSString *> *rowIDs = nil;
if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) {
rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName];
}
self.rows = rows.mutableCopy;
self.rowIDs = rowIDs.mutableCopy;
[self.multiColumnView reloadData];
}
@end
@@ -93,19 +93,40 @@
}
- (void)queryButtonPressed {
[self showQueryInput:nil];
}
- (void)showQueryInput:(NSString *)prefillQuery {
FLEXSQLiteDatabaseManager *database = self.dbm;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Execute an SQL query");
make.textField(nil);
make.configuredTextField(^(UITextField *textField) {
textField.text = prefillQuery;
});
make.button(@"Run").handler(^(NSArray<NSString *> *strings) {
FLEXSQLResult *result = [database executeStatement:strings[0]];
NSString *query = strings[0];
FLEXSQLResult *result = [database executeStatement:query];
if (result.message) {
[FLEXAlert showAlert:@"Message" message:result.message from:self];
// Allow users to edit their last query if it had an error
if ([result.message containsString:@"error"]) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Error").message(result.message);
make.button(@"Edit Query").preferred().handler(^(NSArray<NSString *> *_) {
// Show query editor again with our last input
[self showQueryInput:query];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"Message" message:result.message from:self];
}
} else {
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:result.columns rows:result.rows rowIDs:nil tableName:@"" database:nil
columns:result.columns rows:result.rows
];
[self.navigationController pushViewController:resultsScreen animated:YES];
@@ -0,0 +1,14 @@
//
// FLEXTableRowDataViewController.h
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXFilteringTableViewController.h"
@interface FLEXTableRowDataViewController : FLEXFilteringTableViewController
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData;
@end
@@ -0,0 +1,54 @@
//
// FLEXTableRowDataViewController.m
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXTableRowDataViewController.h"
#import "FLEXMutableListSection.h"
#import "FLEXAlert.h"
@interface FLEXTableRowDataViewController ()
@property (nonatomic) NSDictionary<NSString *, NSString *> *rowsByColumn;
@end
@implementation FLEXTableRowDataViewController
#pragma mark - Initialization
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData {
FLEXTableRowDataViewController *controller = [self new];
controller.rowsByColumn = rowData;
return controller;
}
#pragma mark - Overrides
- (NSArray<FLEXTableViewSection *> *)makeSections {
NSDictionary<NSString *, NSString *> *rowsByColumn = self.rowsByColumn;
FLEXMutableListSection<NSString *> *section = [FLEXMutableListSection list:self.rowsByColumn.allKeys
cellConfiguration:^(UITableViewCell *cell, NSString *column, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = column;
cell.detailTextLabel.text = rowsByColumn[column].description;
} filterMatcher:^BOOL(NSString *filterText, NSString *column) {
return [column localizedCaseInsensitiveContainsString:filterText] ||
[rowsByColumn[column] localizedCaseInsensitiveContainsString:filterText];
}
];
section.selectionHandler = ^(UIViewController *host, NSString *column) {
UIPasteboard.generalPasteboard.string = rowsByColumn[column].description;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Column Copied to Clipboard");
make.message(rowsByColumn[column].description);
make.button(@"Dismiss").cancelStyle();
} showFrom:host];
};
return @[section];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXAPNSViewController.h
// FLEX
//
// Created by Tanner Bennett on 6/28/22.
// Copyright © 2022 FLEX Team. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
#import "FLEXFilteringTableViewController.h"
@interface FLEXAPNSViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end
@@ -0,0 +1,372 @@
//
// FLEXAPNSViewController.m
// FLEX
//
// Created by Tanner Bennett on 6/28/22.
// Copyright © 2022 FLEX Team. All rights reserved.
//
#import "FLEXAPNSViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXMutableListSection.h"
#import "FLEXSingleRowSection.h"
#import "NSUserDefaults+FLEX.h"
#import "UIBarButtonItem+FLEX.h"
#import "NSDateFormatter+FLEX.h"
#import "FLEXResources.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "flex_fishhook.h"
#import <dlfcn.h>
#import <UserNotifications/UserNotifications.h>
#define orig(method, ...) if (orig_##method) { orig_##method(__VA_ARGS__); }
#define method_lookup(__selector, __cls, __return, ...) \
([__cls instancesRespondToSelector:__selector] ? \
(__return(*)(__VA_ARGS__))class_getMethodImplementation(__cls, __selector) : nil)
@interface FLEXAPNSViewController ()
@property (nonatomic, readonly, class) Class appDelegateClass;
@property (nonatomic, class) NSData *deviceToken;
@property (nonatomic, class) NSError *registrationError;
@property (nonatomic, readonly, class) NSString *deviceTokenString;
@property (nonatomic, readonly, class) NSMutableArray<NSDictionary *> *remoteNotifications;
@property (nonatomic, readonly, class) NSMutableArray<UNNotification *> *userNotifications API_AVAILABLE(ios(10.0));
@property (nonatomic) FLEXSingleRowSection *deviceToken;
@property (nonatomic) FLEXMutableListSection<NSDictionary *> *remoteNotifications;
@property (nonatomic) FLEXMutableListSection<UNNotification *> *userNotifications API_AVAILABLE(ios(10.0));
@end
@implementation FLEXAPNSViewController
#pragma mark Swizzles
/// Hook User Notifications related methods on the app delegate
/// and UNUserNotificationCenter delegate classes
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
if (!NSUserDefaults.standardUserDefaults.flex_enableAPNSCapture) {
return;
}
//──────────────────────//
// App Delegate //
//──────────────────────//
// Hook UIApplication to intercept app delegate
Class uiapp = UIApplication.self;
auto orig_uiapp_setDelegate = (void(*)(id, SEL, id))class_getMethodImplementation(
uiapp, @selector(setDelegate:)
);
IMP uiapp_setDelegate = imp_implementationWithBlock(^(id _, id delegate) {
[self hookAppDelegateClass:[delegate class]];
orig_uiapp_setDelegate(_, @selector(setDelegate:), delegate);
});
class_replaceMethod(
uiapp,
@selector(setDelegate:),
uiapp_setDelegate,
"v@:@"
);
//───────────────────────────────────────────//
// UNUserNotificationCenter Delegate //
//───────────────────────────────────────────//
if (@available(iOS 10.0, *)) {
Class unusernc = UNUserNotificationCenter.self;
auto orig_unusernc_setDelegate = (void(*)(id, SEL, id))class_getMethodImplementation(
unusernc, @selector(setDelegate:)
);
IMP unusernc_setDelegate = imp_implementationWithBlock(^(id _, id delegate) {
[self hookUNUserNotificationCenterDelegateClass:[delegate class]];
orig_unusernc_setDelegate(_, @selector(setDelegate:), delegate);
});
class_replaceMethod(
unusernc,
@selector(setDelegate:),
unusernc_setDelegate,
"v@:@"
);
}
}
+ (void)hookAppDelegateClass:(Class)appDelegate {
// Abort if we already hooked something
if (_appDelegateClass) {
return;
}
_appDelegateClass = appDelegate;
// Better documentation for what's happening is in hookUNUserNotificationCenterDelegateClass: below
auto types_didRegisterForRemoteNotificationsWithDeviceToken = "v@:@@";
auto types_didFailToRegisterForRemoteNotificationsWithError = "v@:@@";
auto types_didReceiveRemoteNotification = "v@:@@@?";
auto sel_didRegisterForRemoteNotifications = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
auto sel_didFailToRegisterForRemoteNotifs = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
auto sel_didReceiveRemoteNotification = @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
auto orig_didRegisterForRemoteNotificationsWithDeviceToken = method_lookup(
sel_didRegisterForRemoteNotifications, appDelegate, void, id, SEL, id, id);
auto orig_didFailToRegisterForRemoteNotificationsWithError = method_lookup(
sel_didFailToRegisterForRemoteNotifs, appDelegate, void, id, SEL, id, id);
auto orig_didReceiveRemoteNotification = method_lookup(
sel_didReceiveRemoteNotification, appDelegate, void, id, SEL, id, id, id);
IMP didRegisterForRemoteNotificationsWithDeviceToken = imp_implementationWithBlock(^(id _, id app, NSData *token) {
self.deviceToken = token;
orig(didRegisterForRemoteNotificationsWithDeviceToken, _, nil, app, token);
});
IMP didFailToRegisterForRemoteNotificationsWithError = imp_implementationWithBlock(^(id _, id app, NSError *error) {
self.registrationError = error;
orig(didFailToRegisterForRemoteNotificationsWithError, _, nil, app, error);
});
IMP didReceiveRemoteNotification = imp_implementationWithBlock(^(id _, id app, NSDictionary *payload, id handler) {
// TODO: notify when new notifications are added
[self.remoteNotifications addObject:payload];
orig(didReceiveRemoteNotification, _, nil, app, payload, handler);
});
class_replaceMethod(
appDelegate,
sel_didRegisterForRemoteNotifications,
didRegisterForRemoteNotificationsWithDeviceToken,
types_didRegisterForRemoteNotificationsWithDeviceToken
);
class_replaceMethod(
appDelegate,
sel_didFailToRegisterForRemoteNotifs,
didFailToRegisterForRemoteNotificationsWithError,
types_didFailToRegisterForRemoteNotificationsWithError
);
class_replaceMethod(
appDelegate,
sel_didReceiveRemoteNotification,
didReceiveRemoteNotification,
types_didReceiveRemoteNotification
);
}
+ (void)hookUNUserNotificationCenterDelegateClass:(Class)delegate API_AVAILABLE(ios(10.0)) {
// Selector
auto sel_didReceiveNotification =
@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
// Original implementation (or nil if unimplemented)
auto orig_didReceiveNotification = method_lookup(
sel_didReceiveNotification, delegate, void, id, SEL, id, id, id);
// Our hook (ignores self and other unneeded parameters)
IMP didReceiveNotification = imp_implementationWithBlock(^(id _, id __, UNNotification *notification, id ___) {
[self.userNotifications addObject:notification];
// This macro is a no-op if there is no original implementation
orig(didReceiveNotification, _, nil, __, notification, ___);
});
// Set the hook
class_replaceMethod(
delegate,
sel_didReceiveNotification,
didReceiveNotification,
"v@:@@@?"
);
}
#pragma mark Class Properties
static Class _appDelegateClass = nil;
+ (Class)appDelegateClass {
return _appDelegateClass;
}
static NSData *_apnsDeviceToken = nil;
+ (NSData *)deviceToken {
return _apnsDeviceToken;
}
+ (void)setDeviceToken:(NSData *)deviceToken {
_apnsDeviceToken = deviceToken;
}
+ (NSString *)deviceTokenString {
static NSString *_deviceTokenString = nil;
if (!_deviceTokenString && self.deviceToken) {
NSData *token = self.deviceToken;
NSUInteger capacity = token.length * 2;
NSMutableString *tokenString = [NSMutableString stringWithCapacity:capacity];
const UInt8 *tokenData = token.bytes;
for (NSUInteger idx = 0; idx < token.length; ++idx) {
[tokenString appendFormat:@"%02X", (int)tokenData[idx]];
}
_deviceTokenString = tokenString;
}
return _deviceTokenString;
}
static NSError *_apnsRegistrationError = nil;
+ (NSError *)registrationError {
return _apnsRegistrationError;
}
+ (void)setRegistrationError:(NSError *)error {
_apnsRegistrationError = error;
}
+ (NSMutableArray<NSDictionary *> *)userNotifications {
static NSMutableArray *_userNotifications = nil;
if (!_userNotifications) {
_userNotifications = [NSMutableArray new];
}
return _userNotifications;
}
+ (NSMutableArray<NSDictionary *> *)remoteNotifications {
static NSMutableArray *_remoteNotifications = nil;
if (!_remoteNotifications) {
_remoteNotifications = [NSMutableArray new];
}
return _remoteNotifications;
}
#pragma mark Instance stuff
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Push Notifications";
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget:self action:@selector(reloadData) forControlEvents:UIControlEventValueChanged];
[self addToolbarItems:@[
[UIBarButtonItem
flex_itemWithImage:FLEXResources.gearIcon
target:self
action:@selector(settingsButtonTapped)
],
]];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
self.deviceToken = [FLEXSingleRowSection title:@"APNS Device Token" reuse:nil cell:^(UITableViewCell *cell) {
NSString *tokenString = FLEXAPNSViewController.deviceTokenString;
if (tokenString) {
cell.textLabel.text = tokenString;
cell.textLabel.numberOfLines = 0;
}
else if (!NSUserDefaults.standardUserDefaults.flex_enableAPNSCapture) {
cell.textLabel.text = @"APNS capture disabled";
}
else {
cell.textLabel.text = @"Not yet registered";
}
}];
self.deviceToken.selectionAction = ^(UIViewController *host) {
UIPasteboard.generalPasteboard.string = FLEXAPNSViewController.deviceTokenString;
[FLEXAlert showQuickAlert:@"Copied to Clipboard" from:host];
};
// Remote Notifications //
self.remoteNotifications = [FLEXMutableListSection list:FLEXAPNSViewController.remoteNotifications
cellConfiguration:^(UITableViewCell *cell, NSDictionary *notif, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// TODO: date received
cell.detailTextLabel.text = [FLEXRuntimeUtility summaryForObject:notif];
}
filterMatcher:^BOOL(NSString *filterText, NSDictionary *notif) {
return [notif.description localizedCaseInsensitiveContainsString:filterText];
}
];
self.remoteNotifications.customTitle = @"Remote Notifications";
self.remoteNotifications.selectionHandler = ^(UIViewController *host, NSDictionary *notif) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:notif
] animated:YES];
};
// User Notifications //
if (@available(iOS 10.0, *)) {
self.userNotifications = [FLEXMutableListSection list:FLEXAPNSViewController.userNotifications
cellConfiguration:^(UITableViewCell *cell, UNNotification *notif, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// Subtitle is 'subtitle \n date'
NSString *dateString = [NSDateFormatter flex_stringFrom:notif.date format:FLEXDateFormatPreciseClock];
NSString *subtitle = notif.request.content.subtitle;
subtitle = subtitle ? [NSString stringWithFormat:@"%@\n%@", subtitle, dateString] : dateString;
cell.textLabel.text = notif.request.content.title;
cell.detailTextLabel.text = subtitle;
}
filterMatcher:^BOOL(NSString *filterText, NSDictionary *notif) {
return [notif.description localizedCaseInsensitiveContainsString:filterText];
}
];
self.userNotifications.customTitle = @"Push Notifications";
self.userNotifications.selectionHandler = ^(UIViewController *host, UNNotification *notif) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:notif.request
] animated:YES];
};
return @[self.deviceToken, self.remoteNotifications, self.userNotifications];
}
else {
return @[self.deviceToken, self.remoteNotifications];
}
}
- (void)reloadData {
[self.refreshControl endRefreshing];
self.remoteNotifications.customTitle = [NSString stringWithFormat:
@"%@ notifications", @(self.remoteNotifications.filteredList.count)
];
[super reloadData];
}
- (void)settingsButtonTapped {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
BOOL enabled = defaults.flex_enableAPNSCapture;
NSString *apnsToggle = enabled ? @"Disable Capture" : @"Enable Capture";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Settings")
.message(@"Enable or disable the capture of push notifications.\n\n")
.message(@"This will hook UIApplicationMain on launch until it is disabled, ")
.message(@"and swizzle some app delegate methods. Restart the app for changes to take effect.");
make.button(apnsToggle).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[defaults flex_toggleBoolForKey:kFLEXDefaultsAPNSCaptureEnabledKey];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"📌 Push Notifications";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
@end
@@ -26,6 +26,14 @@
self.title = @"Cookies";
}
- (NSString *)headerTitle {
return self.cookies.title;
}
- (void)setHeaderTitle:(NSString *)headerTitle {
self.cookies.customTitle = headerTitle;
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
@@ -158,7 +158,7 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator subclassesOfClassWithName:className];
NSArray<FLEXObjectRef *> *references = [FLEXRuntimeUtility subclassesOfClassWithName:className];
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
className, @(references.count)
@@ -213,7 +213,7 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}];
}
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title { weakify(self)
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
FLEXMutableListSection *section = [FLEXMutableListSection list:rows
cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
cell.textLabel.text = ref.reference;
@@ -228,8 +228,8 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}
];
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) { strongify(self)
[self.navigationController pushViewController:[
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
};
@@ -109,4 +109,10 @@
_retainer = nil;
}
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %@>",
[self class], self.reference
];
}
@end
@@ -38,9 +38,26 @@
self = [self initWithNibName:nil bundle:nil];
if (self) {
self.originalText = text;
NSString *htmlString = [NSString stringWithFormat:@"<head><style>:root{ color-scheme: light dark; }</style><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
[self.webView loadHTMLString:htmlString baseURL:nil];
NSString *html = @"<head><style>:root{ color-scheme: light dark; }</style>"
"<meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>";
// Loading message for when input text takes a long time to escape
NSString *loadingMessage = [NSString stringWithFormat:html, @"Loading..."];
[self.webView loadHTMLString:loadingMessage baseURL:nil];
// Escape HTML on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *escapedText = [FLEXUtility stringByEscapingHTMLEntitiesInString:text];
NSString *htmlString = [NSString stringWithFormat:html, escapedText];
// Update webview on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.webView loadHTMLString:htmlString baseURL:nil];
});
});
}
return self;
}
@@ -50,14 +67,8 @@
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
}
return self;
}
- (void)dealloc {
// WKWebView's delegate is assigned so we need to clear it manually.
if (_webView.navigationDelegate == self) {
_webView.navigationDelegate = nil;
}
return self;
}
- (void)viewDidLoad {
@@ -68,7 +79,9 @@
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (self.originalText.length > 0) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)
];
}
}
@@ -79,20 +92,23 @@
#pragma mark - WKWebView Delegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
policy = WKNavigationActionPolicyAllow;
} else {
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
// For clicked links, push another web view controller onto the navigation stack
// so that hitting the back button works as expected.
// Don't allow the current web view to handle the navigation.
NSURLRequest *request = navigationAction.request;
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
webVC.title = [[request URL] absoluteString];
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:request.URL];
webVC.title = request.URL.absoluteString;
[self.navigationController pushViewController:webVC animated:YES];
}
decisionHandler(policy);
handler(policy);
}
@@ -101,7 +117,7 @@
+ (BOOL)supportsPathExtension:(NSString *)extension {
BOOL supported = NO;
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
if ([supportedExtensions containsObject:[extension lowercaseString]]) {
if ([supportedExtensions containsObject:extension.lowercaseString]) {
supported = YES;
}
return supported;
@@ -113,11 +129,14 @@
dispatch_once(&onceToken, ^{
// Note that this is not exhaustive, but all these extensions should work well in the web view.
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtensions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
pathExtensions = [NSSet<NSString *> setWithArray:@[
@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"
]];
});
return pathExtensions;
}
@@ -0,0 +1,20 @@
//
// FLEXActivityViewController.h
// FLEX
//
// Created by Tanner Bennett on 5/26/22.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// Wraps UIActivityViewController so that it can't dismiss other view controllers
@interface FLEXActivityViewController : UIActivityViewController
/// @param source A \c UIVIew, \c UIBarButtonItem, or \c NSValue representing a source rect.
+ (id)sharing:(NSArray *)items source:(nullable id)source;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,42 @@
//
// FLEXActivityViewController.m
// FLEX
//
// Created by Tanner Bennett on 5/26/22.
//
#import "FLEXActivityViewController.h"
#import "FLEXMacros.h"
@interface FLEXActivityViewController ()
@end
@implementation FLEXActivityViewController
+ (id)sharing:(NSArray *)items source:(id)sender {
UIViewController *shareSheet = [[UIActivityViewController alloc]
initWithActivityItems:items applicationActivities:nil
];
if (sender && UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
UIPopoverPresentationController *popover = shareSheet.popoverPresentationController;
// Source view
if ([sender isKindOfClass:UIView.self]) {
popover.sourceView = sender;
}
// Source bar item
if ([sender isKindOfClass:UIBarButtonItem.self]) {
popover.barButtonItem = sender;
}
// Source rect
if ([sender isKindOfClass:NSValue.self]) {
CGRect rect = [sender CGRectValue];
popover.sourceRect = rect;
}
}
return shareSheet;
}
@end
@@ -8,7 +8,6 @@
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserController : FLEXTableViewController <FLEXGlobalsEntry>
@@ -9,11 +9,13 @@
#import "FLEXFileBrowserController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXActivityViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import <mach-o/loader.h>
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@@ -264,14 +266,19 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
} else {
// Regardless of file extension...
id object = nil;
@try {
// Try to decode an archived object regardless of file extension
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
} @catch (NSException *e) { }
// Try to decode an archived object, regardless of file extension
NSKeyedUnarchiver *unarchiver = ({
NSKeyedUnarchiver *obj = nil;
if (@available(iOS 12.0, *)) {
obj = [[NSKeyedUnarchiver alloc] initForReadingFromData:fileData error:nil];
} else {
obj = [[NSKeyedUnarchiver alloc] initForReadingWithData:fileData];
}
obj.requiresSecureCoding = NO;
obj;
});
id object = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
// Try to decode other things instead
object = object ?: [NSPropertyListSerialization
propertyListWithData:fileData
@@ -469,10 +476,7 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
[self openFileController:pathString];
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
shareSheet.popoverPresentationController.sourceView = sender;
}
UIViewController *shareSheet = [FLEXActivityViewController sharing:@[filePath] source:sender];
[self presentViewController:shareSheet animated:true completion:nil];
}
}
@@ -19,6 +19,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowCookies,
FLEXGlobalsRowBrowseRuntime,
FLEXGlobalsRowAppKeychainItems,
FLEXGlobalsRowPushNotifications,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
@@ -11,6 +11,7 @@
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcRuntimeViewController.h"
#import "FLEXKeychainViewController.h"
#import "FLEXAPNSViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsController.h"
@@ -57,6 +58,8 @@
switch (row) {
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowPushNotifications:
return [FLEXAPNSViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseRuntime:
@@ -94,13 +97,14 @@
case FLEXGlobalsRowMainThread:
case FLEXGlobalsRowOperationQueue:
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
default:
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:@"Missing globals case in switch" userInfo:nil
];
case FLEXGlobalsRowCount: break;
}
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:@"Missing globals case in switch" userInfo:nil
];
}
+ (NSArray<FLEXGlobalsSection *> *)defaultGlobalSections {
@@ -122,6 +126,7 @@
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
[self globalsEntryForRow:FLEXGlobalsRowPushNotifications],
[self globalsEntryForRow:FLEXGlobalsRowApplication],
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
@@ -28,25 +28,26 @@
if (status == errSecSuccess) {//item already exists, update it!
query = [[NSMutableDictionary alloc]init];
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
#endif
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}else if (status == errSecItemNotFound){//item not found, create it!
}
else if (status == errSecItemNotFound){//item not found, create it!
query = [self query];
if (self.label) {
query[(__bridge id)kSecAttrLabel] = self.label;
}
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
#endif
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
@@ -69,9 +70,9 @@
}
NSMutableDictionary *query = [self query];
#if TARGET_OS_IPHONE
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
#else
// On Mac OS, SecItemDelete will not delete a key created in a different
// app, nor in a different version of the same app.
//
@@ -88,7 +89,7 @@
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
#endif
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
@@ -102,12 +103,12 @@
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnAttributes] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
#endif
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
@@ -149,19 +150,6 @@
#pragma mark - Accessors
- (void)setPasswordObject:(id<NSCoding>)object {
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
}
- (id<NSCoding>)passwordObject {
if (self.passwordData.length) {
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
}
return nil;
}
- (void)setPassword:(NSString *)password {
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
@@ -181,11 +169,11 @@
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+ (BOOL)isSynchronizationAvailable {
#if TARGET_OS_IPHONE
#if TARGET_OS_IPHONE
return YES;
#else
#else
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
#endif
#endif
}
#endif
@@ -204,15 +192,15 @@
dictionary[(__bridge id)kSecAttrAccount] = self.account;
}
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
if (self.accessGroup) {
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
}
#endif
#endif
#endif
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([[self class] isSynchronizationAvailable]) {
id value;
@@ -233,7 +221,7 @@
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
}
#endif
#endif
return dictionary;
}
@@ -251,7 +239,7 @@
case errSecSuccess: return nil;
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
#if TARGET_OS_IPHONE
#if TARGET_OS_IPHONE
case errSecUnimplemented: {
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
break;
@@ -291,10 +279,10 @@
default: {
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
}
#else
#else
default:
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
#endif
#endif
}
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
@@ -122,7 +122,12 @@
path = [NSString stringWithFormat:format, path, path];
}
dlopen(path.UTF8String, RTLD_NOW);
if (!dlopen(path.UTF8String, RTLD_NOW)) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Error").message(@(dlerror()));
make.button(@"Dismiss").cancelStyle();
}];
}
});
} showFrom:self];
}
@@ -26,7 +26,7 @@
*
* See <os/object.h> for details.
*/
#if !TARGET_OS_MACCATALYST
#if !TARGET_OS_MACCATALYST && !__has_include(<xpc/xpc.h>)
#if OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(xpc_object);
#else
@@ -9,6 +9,7 @@
#import "FLEXSystemLogCell.h"
#import "FLEXSystemLogMessage.h"
#import "UIFont+FLEX.h"
#import "NSDateFormatter+FLEX.h"
NSString *const kFLEXSystemLogCellIdentifier = @"FLEXSystemLogCellIdentifier";
@@ -106,14 +107,7 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
}
+ (NSString *)logTimeStringFromDate:(NSDate *)date {
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [NSDateFormatter new];
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
});
return [formatter stringFromDate:date];
return [NSDateFormatter flex_stringFrom:date format:FLEXDateFormatVerbose];
}
@end
@@ -98,7 +98,7 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[super viewDidLoad];
self.showsSearchBar = YES;
self.showSearchBarInitially = NO;
self.pinSearchBar = YES;
weakify(self)
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
@@ -175,9 +175,15 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[self.logMessages mutate:^(NSMutableArray *list) {
[list addObjectsFromArray:newMessages];
}];
// Re-filter messages to filter against new messages
if (self.filterText.length) {
[self updateSearchResults:self.filterText];
}
// "Follow" the log as new messages stream in if we were previously near the bottom.
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
UITableView *tv = self.tableView;
BOOL wasNearBottom = tv.contentOffset.y >= tv.contentSize.height - tv.frame.size.height - 100.0;
[self reloadData];
if (wasNearBottom) {
[self scrollToLastRow];
@@ -187,8 +193,8 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)scrollToLastRow {
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
if (numberOfRows > 0) {
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
NSIndexPath *last = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:last atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
+1
View File
@@ -0,0 +1 @@
../../Classes/GlobalStateExplorers/FileBrowser/FLEXFileBrowserController.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXMetadataExtras.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Categories/NSDateFormatter+FLEX.h
-8
View File
@@ -1,8 +0,0 @@
module FLEX {
umbrella header "FLEX.h"
link "flex"
export *
module * { export * }
}
@@ -52,6 +52,11 @@ NS_ASSUME_NONNULL_BEGIN
/// Removes all registered global entries.
- (void)clearGlobalEntries;
#pragma mark - Editing
/// Enable displaying ivar names for custom struct types
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
#pragma mark - Simulator Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
@@ -15,6 +15,7 @@
#import "FLEXNetworkMITMViewController.h"
#import "FLEXKeyboardHelpViewController.h"
#import "FLEXFileBrowserController.h"
#import "FLEXArgumentInputStructView.h"
#import "FLEXUtility.h"
@interface FLEXManager (ExtensibilityPrivate)
@@ -75,6 +76,13 @@
}
#pragma mark - Editing
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
[FLEXArgumentInputStructView registerFieldNames:names forTypeEncoding:typeEncoding];
}
#pragma mark - Simulator Shortcuts
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description {
+10
View File
@@ -23,12 +23,22 @@ NS_ASSUME_NONNULL_BEGIN
/// Programmatically dismiss anything presented by FLEX, leaving only the toolbar visible.
- (void)dismissAnyPresentedTools:(void (^_Nullable)(void))completion;
/// Programmatically present something on top of the FLEX toolbar.
/// This method will automatically dismiss any currently presented tool,
/// so you do not need to call \c dismissAnyPresentedTools: yourself.
- (void)presentTool:(UINavigationController *(^)(void))viewControllerFuture
completion:(void (^_Nullable)(void))completion;
/// Programmatically presents a new navigation controller with the given view controller.
/// The completion block is passed this new navigation controller.
- (void)presentEmbeddedTool:(UIViewController *)viewController
completion:(void (^_Nullable)(UINavigationController *))completion;
/// Programmatically presents a new navigation controller exploring the given object.
/// The completion block is passed this new navigation controller.
- (void)presentObjectExplorer:(id)object completion:(void (^_Nullable)(UINavigationController *))completion;
/// Use this to present the explorer in a specific scene when the one
/// it chooses by default is not the one you wish to display it in.
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
+16 -1
View File
@@ -10,7 +10,8 @@
#import "FLEXUtility.h"
#import "FLEXExplorerViewController.h"
#import "FLEXWindow.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXFileBrowserController.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@@ -106,6 +107,20 @@
[self.explorerViewController presentTool:future completion:completion];
}
- (void)presentEmbeddedTool:(UIViewController *)tool completion:(void (^)(UINavigationController *))completion {
FLEXNavigationController *nav = [FLEXNavigationController withRootViewController:tool];
[self presentTool:^UINavigationController *{
return nav;
} completion:^{
if (completion) completion(nav);
}];
}
- (void)presentObjectExplorer:(id)object completion:(void (^)(UINavigationController *))completion {
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
[self presentEmbeddedTool:explorer completion:completion];
}
- (void)showExplorerFromScene:(UIWindowScene *)scene {
if (@available(iOS 13.0, *)) {
self.explorerWindow.windowScene = scene;
+300
View File
@@ -0,0 +1,300 @@
//
// 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 {
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
@@ -18,6 +18,7 @@
#import "FLEXManager+Private.h"
#import "FLEXTableView.h"
#import "UIBarButtonItem+FLEX.h"
#import "NSDateFormatter+FLEX.h"
typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
@@ -289,7 +290,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
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]];
NSData *body = [self postBodyDataForTransaction:transaction];
UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:body];
if (detailViewController) {
detailViewController.title = @"Request Body";
return detailViewController;
@@ -297,8 +299,13 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
// 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: ");
if (!body) {
make.title(@"Empty HTTP Body");
} else {
make.title(@"Cannot 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();
}];
@@ -375,19 +382,14 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
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];
localStartTimeRow.detailText = [NSDateFormatter flex_stringFrom:transaction.startTime format:FLEXDateFormatVerbose];
[rows addObject:localStartTimeRow];
startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
FLEXNetworkDetailRow *utcStartTimeRow = [FLEXNetworkDetailRow new];
utcStartTimeRow.title = @"Start Time (UTC)";
utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
utcStartTimeRow.detailText = [NSDateFormatter flex_stringFrom:transaction.startTime format:FLEXDateFormatVerbose];
[rows addObject:utcStartTimeRow];
FLEXNetworkDetailRow *unixStartTime = [FLEXNetworkDetailRow new];
@@ -486,6 +488,10 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
}
+ (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data {
if (!data) {
return nil; // An alert will be presented in place of this screen
}
FLEXCustomContentViewerFuture makeCustomViewer = FLEXManager.sharedManager.customContentTypeViewers[mimeType.lowercaseString];
if (makeCustomViewer) {
@@ -523,11 +529,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
+ (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];
}
if (bodyData.length > 0 && [FLEXUtility hasCompressedContentEncoding:transaction.request]) {
bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
}
return bodyData;
}
+5 -6
View File
@@ -13,22 +13,21 @@ NS_ASSUME_NONNULL_BEGIN
+ (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;
/// Equal to \c allTransactions if not filtered
@property (nonatomic, readonly) NSArray<TransactionType> *filteredTransactions;
/// Use this instead of either of the other two as it updates based on whether we have a filter or not
/// The content of this array is filtered to match the input of \c filter:completion:
@property (nonatomic) NSInteger bytesReceived;
@property (nonatomic) NSInteger totalBytesReceived;
/// Equal to \c totalBytesReceived if not filtered
@property (nonatomic) NSInteger filteredBytesReceived;
- (void)reloadByteCounts;
- (void)reloadData:(void (^_Nullable)(FLEXMITMDataSource *dataSource))completion;
- (void)filter:(NSString *)searchString completion:(void(^_Nullable)(FLEXMITMDataSource *dataSource))completion;
@end
NS_ASSUME_NONNULL_END
+13 -11
View File
@@ -24,12 +24,8 @@
return ds;
}
- (NSArray *)transactions {
return _filteredTransactions;
}
- (NSInteger)bytesReceived {
return _filteredBytesReceived;
- (BOOL)isFiltered {
return self.filterString.length > 0;
}
- (void)reloadByteCounts {
@@ -49,8 +45,9 @@
self.filteredTransactions = self.allTransactions;
if (completion) completion(self);
} else {
NSArray<FLEXNetworkTransaction *> *allTransactions = self.allTransactions.copy;
[self onBackgroundQueue:^NSArray *{
return [self.allTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
return [allTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
return [entry matchesQuery:searchString];
}];
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
@@ -63,15 +60,20 @@
}
- (void)setAllTransactions:(NSArray *)transactions {
_allTransactions = transactions;
_allTransactions = transactions.copy;
[self updateBytesReceived];
}
/// This is really just a semantic setter for \c _transactions
- (void)setFilteredTransactions:(NSArray *)filteredTransactions {
_filteredTransactions = filteredTransactions;
_transactions = filteredTransactions.copy;
[self updateFilteredBytesReceived];
}
- (void)setTransactions:(NSArray *)transactions {
self.filteredTransactions = transactions;
}
- (void)updateBytesReceived {
NSInteger bytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.transactions) {
@@ -83,11 +85,11 @@
- (void)updateFilteredBytesReceived {
NSInteger filteredBytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.filteredTransactions) {
for (FLEXNetworkTransaction *transaction in self.transactions) {
filteredBytesReceived += transaction.receivedDataLength;
}
self.filteredBytesReceived = filteredBytesReceived;
self.bytesReceived = filteredBytesReceived;
}
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
+17 -2
View File
@@ -6,6 +6,7 @@
//
#import "FLEXNetworkCurlLogger.h"
#import "FLEXUtility.h"
@implementation FLEXNetworkCurlLogger
@@ -28,8 +29,22 @@
}
if (request.HTTPBody) {
NSString *body = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
[curlCommandString appendFormat:@"-d \'%@\'", body];
NSData *bodyData = request.HTTPBody;
if ([FLEXUtility hasCompressedContentEncoding:request]) {
bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
}
NSString *body = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
if (body != nil) {
[curlCommandString appendFormat:@"-d \'%@\'", body];
} else {
// Fallback to using base64 encoding
[curlCommandString appendString:@"--data-binary @-"];
NSString *base64 = [request.HTTPBody base64EncodedStringWithOptions:0];
NSString *prefix = [NSString stringWithFormat:@"echo -n '%@' | base64 -D | ", base64];
[curlCommandString insertString:prefix atIndex:0];
}
}
return curlCommandString;
+138 -31
View File
@@ -21,9 +21,14 @@
#import "FLEXWebViewController.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXResources.h"
#import "NSUserDefaults+FLEX.h"
typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
FLEXNetworkObserverModeREST = 0,
#define kFirebaseAvailable NSClassFromString(@"FIRDocumentReference")
#define kWebsocketsAvailable @available(iOS 13.0, *)
typedef NS_ENUM(NSInteger, FLEXNetworkObserverMode) {
FLEXNetworkObserverModeFirebase = 0,
FLEXNetworkObserverModeREST,
FLEXNetworkObserverModeWebsockets,
};
@@ -32,11 +37,12 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
@property (nonatomic) BOOL updateInProgress;
@property (nonatomic) BOOL pendingReload;
@property (nonatomic, readonly) FLEXNetworkObserverMode mode;
@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
@@ -52,20 +58,33 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
[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 (@available(iOS 13.0, *)) {
self.searchController.searchBar.showsScopeBar = YES;
self.searchController.searchBar.scopeButtonTitles = @[@"REST", @"Websockets"];
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
@@ -142,12 +161,23 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
- (void)trashButtonTapped:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear All Recorded Requests?");
make.message(@"This cannot be undone.");
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 All").destructiveStyle().handler(^(NSArray *strings) {
[FLEXNetworkRecorder.defaultRecorder clearRecordedActivity];
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];
}
@@ -160,7 +190,72 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
#pragma mark Transactions
- (FLEXNetworkObserverMode)mode {
return self.searchController.searchBar.selectedScopeButtonIndex;
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 {
@@ -169,9 +264,8 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
return self.HTTPDataSource;
case FLEXNetworkObserverModeWebsockets:
return self.websocketDataSource;
default:
@throw NSInternalInconsistencyException;
case FLEXNetworkObserverModeFirebase:
return self.firebaseDataSource;
}
}
@@ -184,6 +278,7 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
[self.HTTPDataSource reloadData:completion];
[self.websocketDataSource reloadData:completion];
[self.firebaseDataSource reloadData:completion];
}
@@ -199,20 +294,21 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
}
- (NSString *)headerText {
long long bytesReceived = 0;
NSInteger totalRequests = 0;
if (self.searchController.isActive) {
bytesReceived = self.dataSource.filteredBytesReceived;
totalRequests = self.dataSource.transactions.count;
} else {
bytesReceived = self.dataSource.bytesReceived;
totalRequests = self.dataSource.transactions.count;
}
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
];
@@ -236,13 +332,13 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
make.title(@"Network Monitor Disabled");
make.message(@"You must enable network monitoring to proceed.");
make.button(@"Turn On").handler(^(NSArray<NSString *> *strings) {
make.button(@"Turn On").preferred().handler(^(NSArray<NSString *> *strings) {
FLEXNetworkObserver.enabled = YES;
[host.navigationController pushViewController:[
self globalsEntryViewController:row
] animated:YES];
}).cancelStyle();
make.button(@"Dismiss");
});
make.button(@"Dismiss").cancelStyle();
} showFrom:host];
}
};
@@ -332,6 +428,7 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification {
[self.HTTPDataSource reloadByteCounts];
[self.websocketDataSource reloadByteCounts];
// Don't need to reload Firebase here
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
@@ -421,9 +518,13 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
}
break;
}
default:
@throw NSInternalInconsistencyException;
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];
}
}
}
@@ -500,6 +601,9 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
return self.websocketDataSource.transactions[indexPath.row];
}
- (FLEXFirebaseTransaction *)firebaseTransactionAtIndexPath:(NSIndexPath *)indexPath {
return self.firebaseDataSource.transactions[indexPath.row];
}
#pragma mark - Search Bar
@@ -512,11 +616,14 @@ typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
[self.HTTPDataSource filter:searchString completion:callback];
[self.websocketDataSource filter:searchString completion:callback];
[self.firebaseDataSource filter:searchString completion:callback];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)newScope {
[self updateFirstSectionHeader];
[self.tableView reloadData];
NSUserDefaults.standardUserDefaults.flex_lastNetworkObserverMode = self.mode;
}
- (void)willDismissSearchController:(UISearchController *)searchController {
+38 -1
View File
@@ -14,7 +14,14 @@ extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction;
@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction, FLEXFirebaseTransaction;
@class FIRQuery, FIRDocumentReference, FIRCollectionReference, FIRDocumentSnapshot, FIRQuerySnapshot;
typedef NS_ENUM(NSUInteger, FLEXNetworkTransactionKind) {
FLEXNetworkTransactionKindFirebase = 0,
FLEXNetworkTransactionKindREST,
FLEXNetworkTransactionKindWebsockets,
};
@interface FLEXNetworkRecorder : NSObject
@@ -43,6 +50,8 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
@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;
@@ -50,6 +59,9 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
/// 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
@@ -82,4 +94,29 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
- (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
+197 -21
View File
@@ -14,6 +14,13 @@
#import "NSUserDefaults+FLEX.h"
#import "OSCache.h"
#define Synchronized(queue, obj) ({ \
__block id __synchronized_retval = nil; \
dispatch_sync(queue, ^{ __synchronized_retval = obj; }); \
__synchronized_retval; \
})
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
@@ -24,9 +31,10 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
@interface FLEXNetworkRecorder ()
@property (nonatomic) OSCache *restCache;
@property (nonatomic) NSMutableArray<FLEXHTTPTransaction *> *orderedHTTPTransactions;
@property (nonatomic) NSMutableArray<FLEXWebsocketTransaction *> *orderedWSTransactions;
@property (nonatomic) NSMutableDictionary<NSString *, FLEXHTTPTransaction *> *requestIDsToHTTPTransactions;
@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
@@ -47,7 +55,8 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
self.orderedWSTransactions = [NSMutableArray new];
self.orderedHTTPTransactions = [NSMutableArray new];
self.requestIDsToHTTPTransactions = [NSMutableDictionary 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
@@ -82,11 +91,15 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
}
- (NSArray<FLEXHTTPTransaction *> *)HTTPTransactions {
return self.orderedHTTPTransactions.copy;
return Synchronized(self.queue, self.orderedHTTPTransactions.copy);
}
- (NSArray<FLEXWebsocketTransaction *> *)websocketTransactions {
return self.orderedWSTransactions.copy;
return Synchronized(self.queue, self.orderedWSTransactions.copy);
}
- (NSArray<FLEXFirebaseTransaction *> *)firebaseTransactions {
return Synchronized(self.queue, self.orderedFirebaseTransactions.copy);
}
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction {
@@ -98,7 +111,45 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
[self.restCache removeAllObjects];
[self.orderedWSTransactions removeAllObjects];
[self.orderedHTTPTransactions removeAllObjects];
[self.requestIDsToHTTPTransactions 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];
});
@@ -144,10 +195,10 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
}
// A redirect is always a new request
dispatch_async(self.queue, ^{
[self.orderedHTTPTransactions insertObject:transaction atIndex:0];
[self.requestIDsToHTTPTransactions setObject:transaction forKey:requestID];
transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
self.requestIDsToTransactions[requestID] = transaction;
[self postNewTransactionNotificationWithTransaction:transaction];
});
@@ -158,13 +209,13 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
NSDate *responseDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.response = response;
transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
transaction.state = FLEXNetworkTransactionStateReceivingData;
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
[self postUpdateNotificationForTransaction:transaction];
@@ -173,7 +224,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
@@ -187,12 +238,12 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
NSDate *finishedDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.transactionState = FLEXNetworkTransactionStateFinished;
transaction.state = FLEXNetworkTransactionStateFinished;
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
BOOL shouldCache = responseBody.length > 0;
@@ -246,12 +297,12 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
transaction.transactionState = FLEXNetworkTransactionStateFailed;
transaction.state = FLEXNetworkTransactionStateFailed;
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
transaction.error = error;
@@ -261,7 +312,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
dispatch_async(self.queue, ^{
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
FLEXHTTPTransaction *transaction = self.requestIDsToTransactions[requestID];
if (!transaction) {
return;
}
@@ -279,7 +330,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
withMessage:message task:task direction:FLEXWebsocketOutgoing
];
[self.orderedWSTransactions addObject:send];
[self.orderedWSTransactions insertObject:send atIndex:0];
[self postNewTransactionNotificationWithTransaction:send];
});
}
@@ -290,7 +341,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
return t.message == message;
}];
send.error = error;
send.transactionState = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
send.state = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
[self postUpdateNotificationForTransaction:send];
});
@@ -302,12 +353,137 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
withMessage:message task:task direction:FLEXWebsocketIncoming
];
[self.orderedWSTransactions addObject:receive];
[self.orderedWSTransactions insertObject:receive atIndex:0];
[self postNewTransactionNotificationWithTransaction:receive];
});
}
#pragma mark Notification Posting
#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];
@@ -107,7 +107,7 @@
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.hostDenylist.count ? 2 : 1;
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+81 -10
View File
@@ -6,12 +6,13 @@
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "UIKit/UIKit.h"
#import <UIKit/UIKit.h>
#import "Firestore.h"
typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
FLEXNetworkTransactionStateUnstarted,
FLEXNetworkTransactionStateAwaitingResponse,
FLEXNetworkTransactionStateUnstarted = -1,
/// This is the default; it's usually nonsense for a request to be marked as "unstarted"
FLEXNetworkTransactionStateAwaitingResponse = 0,
FLEXNetworkTransactionStateReceivingData,
FLEXNetworkTransactionStateFinished,
FLEXNetworkTransactionStateFailed
@@ -24,7 +25,13 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
/// 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
@interface FLEXNetworkTransaction : NSObject {
@protected
NSString *_primaryDescription;
NSString *_secondaryDescription;
NSString *_tertiaryDescription;
}
+ (instancetype)withStartTime:(NSDate *)startTime;
@@ -35,7 +42,7 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
@property (nonatomic, readonly) BOOL displayAsError;
@property (nonatomic, readonly) NSDate *startTime;
@property (nonatomic) FLEXNetworkTransactionState transactionState;
@property (nonatomic) FLEXNetworkTransactionState state;
@property (nonatomic) int64_t receivedDataLength;
/// A small thumbnail to preview the type of/the response
@property (nonatomic) UIImage *thumbnail;
@@ -48,15 +55,15 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
/// Minor details to display at the bottom of the cell, such as a timestamp, HTTP method, or status.
@property (nonatomic, readonly) NSString *tertiaryDescription;
/// Subclasses should implement for when the transaction is complete
@property (nonatomic, readonly) NSArray<NSString *> *details;
/// 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.
@@ -66,6 +73,8 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
+ (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
@@ -81,7 +90,7 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
@property (nonatomic) NSTimeInterval latency;
@property (nonatomic) NSTimeInterval duration;
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
/// Populated lazily, nullable. Handles both normal HTTPBody data and HTTPBodyStreams.
@property (nonatomic, readonly) NSData *cachedRequestBody;
@end
@@ -105,3 +114,65 @@ typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
@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
+14 -30
View File
@@ -9,16 +9,7 @@
#import "FLEXNetworkTransaction.h"
#import "FLEXResources.h"
#import "FLEXUtility.h"
@interface FLEXNetworkTransaction () {
@protected
NSString *_primaryDescription;
NSString *_secondaryDescription;
NSString *_tertiaryDescription;
}
@end
#import "NSDateFormatter+FLEX.h"
@implementation FLEXNetworkTransaction
@@ -54,6 +45,16 @@
return transaction;
}
- (NSString *)timestampStringFromRequestDate:(NSDate *)date {
return [NSDateFormatter flex_stringFrom:date format:FLEXDateFormatPreciseClock];
}
- (void)setState:(FLEXNetworkTransactionState)transactionState {
_state = transactionState;
// Reset bottom description
_tertiaryDescription = nil;
}
- (BOOL)displayAsError {
return _error != nil;
}
@@ -81,17 +82,6 @@
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];
}
- (NSString *)primaryDescription {
if (!_primaryDescription) {
NSString *name = self.request.URL.lastPathComponent;
@@ -142,11 +132,11 @@
[detailComponents addObject:httpMethod];
}
if (self.transactionState == FLEXNetworkTransactionStateFinished || self.transactionState == FLEXNetworkTransactionStateFailed) {
if (self.state == FLEXNetworkTransactionStateFinished || self.state == FLEXNetworkTransactionStateFailed) {
[detailComponents addObjectsFromArray:self.details];
} else {
// Unstarted, Awaiting Response, Receiving Data, etc.
NSString *state = [self.class readableStringFromTransactionState:self.transactionState];
NSString *state = [self.class readableStringFromTransactionState:self.state];
[detailComponents addObject:state];
}
@@ -156,12 +146,6 @@
return _tertiaryDescription;
}
- (void)setTransactionState:(FLEXNetworkTransactionState)transactionState {
super.transactionState = transactionState;
// Reset bottom description
_tertiaryDescription = nil;
}
- (NSString *)copyString {
return self.request.URL.absoluteString;
}
@@ -261,7 +245,7 @@
// Populate receivedDataLength
if (direction == FLEXWebsocketIncoming) {
wst.receivedDataLength = wst.dataLength;
wst.transactionState = FLEXNetworkTransactionStateFinished;
wst.state = FLEXNetworkTransactionStateFinished;
}
// Populate thumbnail image
@@ -37,6 +37,7 @@ NSString * const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransacti
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];
+190
View File
@@ -0,0 +1,190 @@
//
// Firestore.h
// Pods
//
// Created by Tanner Bennett on 10/13/21.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Forward Declarations
@class FIRQuery;
@class FIRQuerySnapshot;
@class FIRDocumentReference;
@class FIRDocumentSnapshot;
@class FIRQueryDocumentSnapshot;
@class FIRCollectionReference;
@class FIRFirestore;
@protocol FIRListenerRegistration;
#define cFIRQuery objc_getClass("FIRQuery")
#define cFIRCollectionReference objc_getClass("FIRCollectionReference")
#define cFIRDocumentReference objc_getClass("FIRDocumentReference")
typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot,
NSError *_Nullable error);
typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot,
NSError *_Nullable error);
typedef NS_ENUM(NSUInteger, FIRFirestoreSource) {
FIRFirestoreSourceDefault,
FIRFirestoreSourceServer,
FIRFirestoreSourceCache
} NS_SWIFT_NAME(FirestoreSource);
#pragma mark - Query
@interface FIRQuery : NSObject
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) FIRFirestore *firestore;
@property(nonatomic, readonly) void *query;
- (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion
NS_SWIFT_NAME(getDocuments(completion:));
- (void)getDocumentsWithSource:(FIRFirestoreSource)source
completion:(FIRQuerySnapshotBlock)completion
NS_SWIFT_NAME(getDocuments(source:completion:));
@end
typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot,
NSError *_Nullable error);
#pragma mark - DocumentReference
NS_SWIFT_NAME(DocumentReference)
@interface FIRDocumentReference : NSObject
- (instancetype)init __attribute__((unavailable));
@property(nonatomic, readonly) NSString *documentID;
@property(nonatomic, readonly) FIRCollectionReference *parent;
@property(nonatomic, readonly) FIRFirestore *firestore;
@property(nonatomic, readonly) NSString *path;
- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
NS_SWIFT_NAME(collection(_:));
#pragma mark Writing Data
- (void)setData:(NSDictionary<NSString *, id> *)documentData;
- (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge;
- (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
merge:(BOOL)merge
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)setData:(NSDictionary<NSString *, id> *)documentData
mergeFields:(NSArray<id> *)mergeFields
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)updateData:(NSDictionary<id, id> *)fields;
- (void)updateData:(NSDictionary<id, id> *)fields
completion:(nullable void (^)(NSError *_Nullable error))completion;
- (void)deleteDocument NS_SWIFT_NAME(delete());
- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion
NS_SWIFT_NAME(delete(completion:));
#pragma mark Retrieving Data
- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion
NS_SWIFT_NAME(getDocument(completion:));
- (void)getDocumentWithSource:(FIRFirestoreSource)source
completion:(FIRDocumentSnapshotBlock)completion
NS_SWIFT_NAME(getDocument(source:completion:));
- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener
NS_SWIFT_NAME(addSnapshotListener(_:));
- (id<FIRListenerRegistration>)addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
listener:(FIRDocumentSnapshotBlock)listener
NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:));
@end
#pragma mark - CollectionReference
NS_SWIFT_NAME(CollectionReference)
@interface FIRCollectionReference : FIRQuery
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) NSString *collectionID;
@property(nonatomic, nullable, readonly) FIRDocumentReference *parent;
@property(nonatomic, readonly) NSString *path;
- (FIRDocumentReference *)documentWithAutoID NS_SWIFT_NAME(document());
- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath NS_SWIFT_NAME(document(_:));
- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
NS_SWIFT_NAME(addDocument(data:));
- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
completion:(nullable void (^)(NSError *_Nullable error))completion
NS_SWIFT_NAME(addDocument(data:completion:));
@end
#pragma mark - QuerySnapshot
NS_SWIFT_NAME(QuerySnapshot)
@interface FIRQuerySnapshot : NSObject
- (id)init __attribute__((unavailable()));
@property(nonatomic, readonly) FIRQuery *query;
@property(nonatomic, readonly, getter=isEmpty) BOOL empty;
@property(nonatomic, readonly) NSInteger count;
@property(nonatomic, readonly) NSArray<FIRQueryDocumentSnapshot *> *documents;
@end
#pragma mark - DocumentSnapshot
NS_SWIFT_NAME(DocumentSnapshot)
@interface FIRDocumentSnapshot : NSObject
- (instancetype)init __attribute__((unavailable()));
@property(nonatomic, readonly) BOOL exists;
@property(nonatomic, readonly) FIRDocumentReference *reference;
@property(nonatomic, copy, readonly) NSString *documentID;
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, id> *data;
- (nullable id)valueForField:(id)field NS_SWIFT_NAME(get(_:));
- (nullable id)objectForKeyedSubscript:(id)key;
@end
#pragma mark - QueryDocumentSnapshot
NS_SWIFT_NAME(QueryDocumentSnapshot)
@interface FIRQueryDocumentSnapshot : FIRDocumentSnapshot
- (instancetype)init __attribute__((unavailable()));
@property(nonatomic, readonly) NSDictionary<NSString *, id> *data;
@end
NS_ASSUME_NONNULL_END
#if defined(__clang__)
#if __has_feature(objc_arc)
#define _LOGOS_SELF_TYPE_NORMAL __unsafe_unretained
#define _LOGOS_SELF_TYPE_INIT __attribute__((ns_consumed))
#define _LOGOS_SELF_CONST const
#define _LOGOS_RETURN_RETAINED __attribute__((ns_returns_retained))
#else
#define _LOGOS_SELF_TYPE_NORMAL
#define _LOGOS_SELF_TYPE_INIT
#define _LOGOS_SELF_CONST
#define _LOGOS_RETURN_RETAINED
#endif
#else
#define _LOGOS_SELF_TYPE_NORMAL
#define _LOGOS_SELF_TYPE_INIT
#define _LOGOS_SELF_CONST
#define _LOGOS_RETURN_RETAINED
#endif
@@ -21,7 +21,9 @@
#import "NSUserDefaults+FLEX.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXMethod.h"
#import "Firestore.h"
#import <AVFoundation/AVFoundation.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import <dispatch/queue.h>
@@ -101,7 +103,7 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
if (enabled) {
// Inject if needed. This injection is protected with a dispatch_once, so we're ok calling it multiple times.
// By doing the injection lazily, we keep the impact of the tool lower when this feature isn't enabled.
[self injectIntoAllNSURLThings];
[self setNetworkMonitorHooks];
}
if (previouslyEnabled != enabled) {
@@ -114,10 +116,13 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
}
+ (void)load {
// We don't want to do the swizzling from +load because not all the classes may be loaded at this point.
// We don't want to do the swizzling from +load because not all the
// delegate classes we want to hook may be loaded at this point.
// However, Firebase classes will definitely be loaded by now,
// so we can definitely hook those sooner if need be.
dispatch_async(dispatch_get_main_queue(), ^{
if ([self isEnabled]) {
[self injectIntoAllNSURLThings];
[self setNetworkMonitorHooks];
}
});
}
@@ -166,7 +171,299 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - Delegate Injection
#pragma mark - Hooking
static void (*_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock);
static void _logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock);
static void (*_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$)(
_LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock);
static void _logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$(
_LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock);
static void (*_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, BOOL, void (^)(NSError *));
static void (*_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, NSArray *, void (^)(NSError *));
static void (*_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, void (^)(NSError *));
static void (*_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, void (^)(NSError *));
static void _logos_register_hook(Class _class, SEL _cmd, IMP _new, IMP *_old) {
unsigned int _count, _i;
Class _searchedClass = _class;
Method *_methods;
while (_searchedClass) {
_methods = class_copyMethodList(_searchedClass, &_count);
for (_i = 0; _i < _count; _i++) {
if (method_getName(_methods[_i]) == _cmd) {
if (_class == _searchedClass) {
*_old = method_getImplementation(_methods[_i]);
*_old = class_replaceMethod(_class, _cmd, _new, method_getTypeEncoding(_methods[_i]));
} else {
class_addMethod(_class, _cmd, _new, method_getTypeEncoding(_methods[_i]));
}
free(_methods);
return;
}
}
free(_methods);
_searchedClass = class_getSuperclass(_searchedClass);
}
}
static Class _logos_superclass$_ungrouped$FIRDocumentReference;
static void (*_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$)(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock);
static Class _logos_superclass$_ungrouped$FIRQuery;
static void (*_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$)(
_LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock);
static Class _logos_superclass$_ungrouped$FIRCollectionReference;
static FIRDocumentReference * (*_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$)(
_LOGOS_SELF_TYPE_NORMAL FIRCollectionReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, void (^)(NSError *error));
#pragma mark Firebase, Reading Data
static void _logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST self, SEL _cmd, FIRDocumentSnapshotBlock completion) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder recordFIRDocumentWillFetch:self withTransactionID:requestID];
// Hook callback
FIRDocumentSnapshotBlock orig = completion;
completion = ^(FIRDocumentSnapshot *document, NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDocumentDidFetch:document error:error transactionID:requestID];
if (orig != nil) {
orig(document, error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$ ? _logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(getDocumentWithCompletion:)))(self, _cmd, completion);
}
static void _logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$(
_LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST self, SEL _cmd, FIRQuerySnapshotBlock completion) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder recordFIRQueryWillFetch:self withTransactionID:requestID];
// Hook callback
FIRQuerySnapshotBlock orig = completion;
completion = ^(FIRQuerySnapshot *query, NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRQueryDidFetch:query error:error transactionID:requestID];
if (orig != nil) {
orig(query, error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$ ? _logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRQuery, @selector(getDocumentsWithCompletion:)))(self, _cmd, completion);
}
#pragma mark Firebase, Writing Data
static void _logos_method$_ungrouped$FIRDocumentReference$setData$merge$completion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self,
SEL __unused _cmd, NSDictionary<NSString *, id> * documentData, BOOL merge, void (^completion)(NSError *)) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder
recordFIRWillSetData:self
data:documentData
merge:@(merge)
mergeFields:nil
transactionID:requestID
];
// Hook callback
void (^orig)(NSError *) = completion;
completion = ^(NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDidSetData:error transactionID:requestID];
if (orig != nil) {
orig(error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(setData:merge:completion:)))(self, _cmd, documentData, merge, completion);
}
static void _logos_method$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self,
SEL __unused _cmd, NSDictionary<NSString *, id> * documentData,
NSArray * mergeFields, void (^completion)(NSError *)) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder
recordFIRWillSetData:self
data:documentData
merge:nil
mergeFields:mergeFields
transactionID:requestID
];
// Hook callback
void (^orig)(NSError *) = completion;
completion = ^(NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDidSetData:error transactionID:requestID];
if (orig != nil) {
orig(error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(setData:mergeFields:completion:)))(self, _cmd, documentData, mergeFields, completion);
}
static void _logos_method$_ungrouped$FIRDocumentReference$updateData$completion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self,
SEL __unused _cmd, NSDictionary<id, id> * fields, void (^completion)(NSError *)) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder recordFIRWillUpdateData:self fields:fields transactionID:requestID];
// Hook callback
void (^orig)(NSError *) = completion;
completion = ^(NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDidUpdateData:error transactionID:requestID];
if (orig != nil) {
orig(error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(updateData:completion:)))(self, _cmd, fields, completion);
}
static void _logos_method$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$(
_LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self,
SEL __unused _cmd, void (^completion)(NSError *)) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder recordFIRWillDeleteDocument:self transactionID:requestID];
// Hook callback
void (^orig)(NSError *) = completion;
completion = ^(NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDidDeleteDocument:error transactionID:requestID];
if (orig != nil) {
orig(error);
}
};
// Forward invocation
(_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$ ? _logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(deleteDocumentWithCompletion:)))(self, _cmd, completion);
}
static FIRDocumentReference * _logos_method$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$(
_LOGOS_SELF_TYPE_NORMAL FIRCollectionReference * _LOGOS_SELF_CONST __unused self,
SEL __unused _cmd, NSDictionary<NSString *, id> * data, void (^completion)(NSError *error)) {
// Generate transaction ID
NSString *requestID = [FLEXNetworkObserver nextRequestID];
// Hook callback
void (^orig)(NSError *) = completion;
completion = ^(NSError *error) {
[FLEXNetworkRecorder.defaultRecorder recordFIRDidAddDocument:error transactionID:requestID];
if (orig != nil) {
orig(error);
}
};
// Forward invocation
FIRDocumentReference *ret = (_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$ ? _logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRCollectionReference, @selector(addDocumentWithData:completion:)))(self, _cmd, data, completion);
// Record transaction start
[FLEXNetworkRecorder.defaultRecorder recordFIRWillAddDocument:self document:ret transactionID:requestID];
// Return
return ret;
}
+ (void)setNetworkMonitorHooks {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hookFirebaseThings];
[self injectIntoAllNSURLThings];
});
}
+ (void)hookFirebaseThings {
Class _logos_class$_ungrouped$FIRDocumentReference = objc_getClass("FIRDocumentReference");
_logos_superclass$_ungrouped$FIRDocumentReference = class_getSuperclass(_logos_class$_ungrouped$FIRDocumentReference);
Class _logos_class$_ungrouped$FIRQuery = objc_getClass("FIRQuery");
_logos_superclass$_ungrouped$FIRQuery = class_getSuperclass(_logos_class$_ungrouped$FIRQuery);
Class _logos_class$_ungrouped$FIRCollectionReference = objc_getClass("FIRCollectionReference");
_logos_superclass$_ungrouped$FIRCollectionReference = class_getSuperclass(_logos_class$_ungrouped$FIRCollectionReference);
// Reading //
_logos_register_hook(
_logos_class$_ungrouped$FIRDocumentReference,
@selector(getDocumentWithCompletion:),
(IMP)&_logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$,
(IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$
);
_logos_register_hook(
_logos_class$_ungrouped$FIRQuery,
@selector(getDocumentsWithCompletion:),
(IMP)&_logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$,
(IMP *)&_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$
);
// Writing //
_logos_register_hook(
_logos_class$_ungrouped$FIRDocumentReference,
@selector(setData:merge:completion:),
(IMP)&_logos_method$_ungrouped$FIRDocumentReference$setData$merge$completion$,
(IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$
);
_logos_register_hook(
_logos_class$_ungrouped$FIRDocumentReference,
@selector(setData:mergeFields:completion:),
(IMP)&_logos_method$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$,
(IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$
);
_logos_register_hook(
_logos_class$_ungrouped$FIRDocumentReference,
@selector(updateData:completion:),
(IMP)&_logos_method$_ungrouped$FIRDocumentReference$updateData$completion$,
(IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$
);
_logos_register_hook(
_logos_class$_ungrouped$FIRDocumentReference,
@selector(deleteDocumentWithCompletion:),
(IMP)&_logos_method$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$,
(IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$
);
_logos_register_hook(
_logos_class$_ungrouped$FIRCollectionReference,
@selector(addDocumentWithData:completion:),
(IMP)&_logos_method$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$,
(IMP *)&_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$
);
}
+ (void)injectIntoAllNSURLThings {
// Only allow swizzling once.
@@ -236,8 +533,17 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
[self injectIntoNSURLConnectionAsynchronousClassMethod];
[self injectIntoNSURLConnectionSynchronousClassMethod];
[self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods];
[self injectIntoNSURLSessionAsyncUploadTaskMethods];
Class URLSession = [NSURLSession class];
[self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods:URLSession];
[self injectIntoNSURLSessionAsyncUploadTaskMethods:URLSession];
// At some point, NSURLSession.sharedSession became an __NSURLSessionLocal,
// which is not the class returned by [NSURLSession class], of course
Class URLSessionLocal = NSClassFromString(@"__NSURLSessionLocal");
if (URLSessionLocal && (URLSession != URLSessionLocal)) {
[self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods:URLSessionLocal];
[self injectIntoNSURLSessionAsyncUploadTaskMethods:URLSessionLocal];
}
if (@available(iOS 13.0, *)) {
Class websocketTask = NSClassFromString(@"__NSURLSessionWebSocketTask");
@@ -361,15 +667,22 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
Method originalResume = class_getInstanceMethod(class, selector);
IMP implementation = imp_implementationWithBlock(^(NSURLSessionTask *slf) {
// iOS's internal HTTP parser finalization code is mysteriously not thread safe,
// invoking it asynchronously has a chance to cause a `double free` crash.
// This line below will ask for HTTPBody synchronously, make the HTTPParser
// parse the request, and cache them in advance. After that the HTTPParser
// will be finalized. Make sure other threads inspecting the request
// won't trigger a race to finalize the parser.
[slf.currentRequest HTTPBody];
if (@available(iOS 11.0, *)) {
// AVAggregateAssetDownloadTask deeply does not like to be looked at. Accessing -currentRequest or
// -originalRequest will crash. Do not try to observe these. https://github.com/FLEXTool/FLEX/issues/276
if (![slf isKindOfClass:[AVAggregateAssetDownloadTask class]]) {
// iOS's internal HTTP parser finalization code is mysteriously not thread safe,
// invoking it asynchronously has a chance to cause a `double free` crash.
// This line below will ask for HTTPBody synchronously, make the HTTPParser
// parse the request, and cache them in advance. After that the HTTPParser
// will be finalized. Make sure other threads inspecting the request
// won't trigger a race to finalize the parser.
[slf.currentRequest HTTPBody];
[FLEXNetworkObserver.sharedObserver URLSessionTaskWillResume:slf];
}
}
[FLEXNetworkObserver.sharedObserver URLSessionTaskWillResume:slf];
((void(*)(id, SEL))objc_msgSend)(
slf, swizzledSelector
);
@@ -521,11 +834,11 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
});
}
+ (void)injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods {
+ (void)injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods:(Class)sessionClass {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [NSURLSession class];
Class class = sessionClass;
// The method signatures here are close enough that
// we can use the same logic to inject into all of them.
const SEL selectors[] = {
@@ -589,11 +902,11 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
});
}
+ (void)injectIntoNSURLSessionAsyncUploadTaskMethods {
+ (void)injectIntoNSURLSessionAsyncUploadTaskMethods:(Class)sessionClass {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [NSURLSession class];
Class class = sessionClass;
// The method signatures here are close enough that we can use the same logic to inject into both of them.
// Note that they have 3 arguments, so we can't easily combine with the data and download method above.
typedef NSURLSessionUploadTask *(^UploadTaskMethod)(
@@ -1309,15 +1622,19 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
[FLEXNetworkObserver.sharedObserver
websocketTask:slf sendMessagage:message
];
completion = ^(NSError *error) {
id completionHook = ^(NSError *error) {
[FLEXNetworkObserver.sharedObserver
websocketTaskMessageSendCompletion:message
error:error
];
if (completion) {
completion(error);
}
};
((void(*)(id, SEL, id, id))objc_msgSend)(
slf, swizzledSelector, message, completion
slf, swizzledSelector, message, completionHook
);
};
@@ -1586,6 +1903,12 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
NSString *requestID = [[self class] requestIDForConnectionOrTask:dataTask];
FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID];
// Fix for "Response body not in cache" issue reported by developers
// See this github comment for detailed explanation on why this happens
// https://github.com/FLEXTool/FLEX/issues/568#issuecomment-1141015572
if (requestState.dataAccumulator == nil) {
requestState.dataAccumulator = [NSMutableData new];
}
[requestState.dataAccumulator appendData:data];
[FLEXNetworkRecorder.defaultRecorder
@@ -1664,6 +1987,14 @@ didFinishDownloadingToURL:(NSURL *)location data:(NSData *)data
}
- (void)URLSessionTaskWillResume:(NSURLSessionTask *)task {
if (@available(iOS 11.0, *)) {
// AVAggregateAssetDownloadTask deeply does not like to be looked at. Accessing -currentRequest or
// -originalRequest will crash. Do not try to observe these. https://github.com/FLEXTool/FLEX/issues/276
if ([task isKindOfClass:[AVAggregateAssetDownloadTask class]]) {
return;
}
}
// Since resume can be called multiple times on the same task, only treat the first resume as
// the equivalent to connection:willSendRequest:...
[self performBlock:^{
+9 -7
View File
@@ -72,23 +72,21 @@
return self;
}
- (id<FLEXMirror>)initialMirror {
- (id<FLEXMirror>)mirrorForClass:(Class)cls {
static Class FLEXSwiftMirror = nil;
id obj = _object;
// Should we use Reflex?
if (FLEXIsSwiftObjectOrClass(obj) && FLEXObjectExplorer.reflexAvailable) {
if (FLEXIsSwiftObjectOrClass(cls) && FLEXObjectExplorer.reflexAvailable) {
// Initialize FLEXSwiftMirror class if needed
if (!FLEXSwiftMirror) {
FLEXSwiftMirror = NSClassFromString(@"FLEXSwiftMirror");
}
return [(id<FLEXMirror>)[FLEXSwiftMirror alloc] initWithSubject:obj];
return [(id<FLEXMirror>)[FLEXSwiftMirror alloc] initWithSubject:cls];
}
// No; not swift object, or Reflex unavailable
return [FLEXMirror reflect:obj];
return [FLEXMirror reflect:cls];
}
@@ -178,9 +176,9 @@
Class superclass = nil;
NSInteger count = self.classHierarchyClasses.count;
NSInteger rootIdx = count - 1;
id<FLEXMirror> mirror = self.initialMirror;
for (NSInteger i = 0; i < count; i++) {
Class cls = self.classHierarchyClasses[i];
id<FLEXMirror> mirror = [self mirrorForClass:cls];
superclass = (i < rootIdx) ? self.classHierarchyClasses[i+1] : nil;
[allProperties addObject:[self
@@ -355,6 +353,10 @@
if ([names containsObject:name]) {
return NO;
} else {
if (!name) {
return NO;
}
[names addObject:name];
// Skip methods and properties which are just overrides,
@@ -10,6 +10,7 @@
#import "FLEXGlobalsViewController.h"
#import "FLEXClassShortcuts.h"
#import "FLEXViewShortcuts.h"
#import "FLEXWindowShortcuts.h"
#import "FLEXViewControllerShortcuts.h"
#import "FLEXUIAppShortcuts.h"
#import "FLEXImageShortcuts.h"
@@ -48,6 +49,7 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
ClassKey(UIViewController) : [FLEXViewControllerShortcuts class],
ClassKey(UIApplication) : [FLEXUIAppShortcuts class],
ClassKey(UIView) : [FLEXViewShortcuts class],
ClassKey(UIWindow) : [FLEXWindowShortcuts class],
ClassKey(UIImage) : [FLEXImageShortcuts class],
ClassKey(CALayer) : [FLEXLayerShortcuts class],
ClassKey(UIColor) : [FLEXColorPreviewSection class],
@@ -180,7 +182,7 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
return [self explorerViewControllerForObject:UIDevice.currentDevice];
case FLEXGlobalsRowPasteboard:
return [self explorerViewControllerForObject:UIPasteboard.generalPasteboard];
case FLEXGlobalsRowURLSession:
case FLEXGlobalsRowURLSession:
return [self explorerViewControllerForObject:NSURLSession.sharedSession];
case FLEXGlobalsRowURLCache:
return [self explorerViewControllerForObject:NSURLCache.sharedURLCache];
@@ -215,8 +217,22 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
return nil;
}
default: return nil;
case FLEXGlobalsRowNetworkHistory:
case FLEXGlobalsRowSystemLog:
case FLEXGlobalsRowLiveObjects:
case FLEXGlobalsRowAddressInspector:
case FLEXGlobalsRowCookies:
case FLEXGlobalsRowBrowseRuntime:
case FLEXGlobalsRowAppKeychainItems:
case FLEXGlobalsRowPushNotifications:
case FLEXGlobalsRowBrowseBundle:
case FLEXGlobalsRowBrowseContainer:
case FLEXGlobalsRowCount:
return nil;
}
return nil;
}
+ (FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
@@ -109,7 +109,7 @@
if (!self.filterText.length) {
self.metadata = self.allMetadata;
} else {
self.metadata = [self.allMetadata flex_filtered:^BOOL(FLEXProperty *obj, NSUInteger idx) {
self.metadata = [self.allMetadata flex_filtered:^BOOL(id<FLEXRuntimeMetadata> obj, NSUInteger idx) {
return [obj.description localizedCaseInsensitiveContainsString:self.filterText];
}];
}
@@ -36,7 +36,7 @@ typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id
/// By default, rows are not selectable. If you want rows
/// to be selectable, provide a selection handler here.
@property (nonatomic, copy) void (^selectionHandler)(__kindof UIViewController *host, id element);
@property (nonatomic, copy) void (^selectionHandler)(__kindof UIViewController *host, ObjectType element);
/// The objects representing all possible rows in the section.
@property (nonatomic) NSArray<ObjectType> *list;
@@ -55,4 +55,3 @@ typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id
- (void)mutate:(void(^)(NSMutableArray *list))block;
@end
@@ -77,7 +77,8 @@
@"frame", @"bounds", @"center", @"transform",
@"backgroundColor", @"alpha", @"opaque", @"hidden",
@"clipsToBounds", @"userInteractionEnabled", @"layer",
@"superview", @"subviews"
@"superview", @"subviews",
@"accessibilityIdentifier", @"accessibilityLabel"
]).forClass(UIView.class);
// UILabel
@@ -86,7 +87,8 @@
@"textColor", @"textAlignment", @"numberOfLines",
@"lineBreakMode", @"enabled", @"backgroundColor",
@"alpha", @"hidden", @"preferredMaxLayoutWidth",
@"superview", @"subviews"
@"superview", @"subviews",
@"accessibilityIdentifier", @"accessibilityLabel"
]).forClass(UILabel.class);
// UIWindow
@@ -111,14 +113,16 @@
self.append.ivars(ivars).methods(methods).properties(@[
@"enabled", @"allTargets", @"frame",
@"backgroundColor", @"hidden", @"clipsToBounds",
@"userInteractionEnabled", @"superview", @"subviews"
@"userInteractionEnabled", @"superview", @"subviews",
@"accessibilityIdentifier", @"accessibilityLabel"
]).forClass(UIControl.class);
// UIButton
self.append.ivars(ivars).properties(@[
@"titleLabel", @"font", @"imageView", @"tintColor",
@"currentTitle", @"currentImage", @"enabled", @"frame",
@"superview", @"subviews"
@"superview", @"subviews",
@"accessibilityIdentifier", @"accessibilityLabel"
]).forClass(UIButton.class);
// UIImageView
@@ -126,6 +130,7 @@
@"image", @"animationImages", @"frame", @"bounds", @"center",
@"transform", @"alpha", @"hidden", @"clipsToBounds",
@"userInteractionEnabled", @"layer", @"superview", @"subviews",
@"accessibilityIdentifier", @"accessibilityLabel"
]).forClass(UIImageView.class);
}
@@ -165,10 +165,10 @@
// Generate all (sub)titles from shortcuts
if (self.allShortcuts) {
self.allTitles = [self.allShortcuts flex_mapped:^id(FLEXShortcut *s, NSUInteger idx) {
self.allTitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) {
return [s titleWith:self.object];
}];
self.allSubtitles = [self.allShortcuts flex_mapped:^id(FLEXShortcut *s, NSUInteger idx) {
self.allSubtitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) {
return [s subtitleWith:self.object] ?: @"";
}];
}
@@ -188,7 +188,7 @@
- (BOOL)canSelectRow:(NSInteger)row {
UITableViewCellAccessoryType type = [self.shortcuts[row] accessoryTypeWith:self.object];
BOOL hasDisclosure = NO;
hasDisclosure |= type == UITableViewCellAccessoryDisclosureIndicator;
hasDisclosure |= type == UITableViewCellAccessoryDisclosureIndicator;
hasDisclosure |= type == UITableViewCellAccessoryDetailDisclosureButton;
return hasDisclosure;
}
@@ -0,0 +1,13 @@
//
// FLEXWindowShortcuts.h
// FLEX
//
// Created by AnthoPak on 26/09/2022.
//
#import "FLEXShortcutsSection.h"
/// Adds "Animations Speed" shortcut for all windows
@interface FLEXWindowShortcuts : FLEXShortcutsSection
@end
@@ -0,0 +1,48 @@
//
// FLEXWindowShortcuts.m
// FLEX
//
// Created by AnthoPak on 26/09/2022.
//
#import "FLEXWindowShortcuts.h"
#import "FLEXShortcut.h"
#import "FLEXAlert.h"
#import "FLEXObjectExplorerViewController.h"
@implementation FLEXWindowShortcuts
#pragma mark - Overrides
+ (instancetype)forObject:(UIView *)view {
return [self forObject:view additionalRows:@[
[FLEXActionShortcut title:@"Animation Speed" subtitle:^NSString *(UIWindow *window) {
return [NSString stringWithFormat:@"Current speed: %.2f", window.layer.speed];
} selectionHandler:^(UIViewController *host, UIWindow *window) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Change Animation Speed");
make.message([NSString stringWithFormat:@"Current speed: %.2f", window.layer.speed]);
make.configuredTextField(^(UITextField * _Nonnull textField) {
textField.placeholder = @"Default: 1.0";
textField.keyboardType = UIKeyboardTypeDecimalPad;
});
make.button(@"OK").handler(^(NSArray<NSString *> *strings) {
NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
CGFloat speedValue = [formatter numberFromString:strings.firstObject].floatValue;
window.layer.speed = speedValue;
// Refresh the host view controller to update the shortcut subtitle, reflecting current speed
// TODO: this shouldn't be necessary
[(FLEXObjectExplorerViewController *)host reloadData];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
} accessoryType:^UITableViewCellAccessoryType(id _Nonnull object) {
return UITableViewCellAccessoryDisclosureIndicator;
}]
]];
}
@end
+1 -1
View File
@@ -33,7 +33,7 @@
+ (instancetype)itemWithTitle:(NSString *)title image:(UIImage *)image sibling:(FLEXExplorerToolbarItem *)backupItem {
NSParameterAssert(title); NSParameterAssert(image);
FLEXExplorerToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeSystem];
FLEXExplorerToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeCustom];
toolbarItem.sibling = backupItem;
toolbarItem.title = title;
toolbarItem.image = image;
@@ -0,0 +1,24 @@
//
// NSDateFormatter+FLEX.h
// libflex:FLEX
//
// Created by Tanner Bennett on 7/24/22.
// Copyright © 2022 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, FLEXDateFormat) {
// hour:minute [AM|PM]
FLEXDateFormatClock,
// hour:minute:second [AM|PM]
FLEXDateFormatPreciseClock,
// year-month-day hour:minute:second.millisecond
FLEXDateFormatVerbose,
};
@interface NSDateFormatter (FLEX)
+ (NSString *)flex_stringFrom:(NSDate *)date format:(FLEXDateFormat)format;
@end
@@ -0,0 +1,34 @@
//
// NSDateFormatter+FLEX.m
// libflex:FLEX
//
// Created by Tanner Bennett on 7/24/22.
// Copyright © 2022 Flipboard. All rights reserved.
//
#import "NSDateFormatter+FLEX.h"
@implementation NSDateFormatter (FLEX)
+ (NSString *)flex_stringFrom:(NSDate *)date format:(FLEXDateFormat)format {
static NSDateFormatter *formatter = nil;
if (!formatter) {
formatter = [NSDateFormatter new];
}
switch (format) {
case FLEXDateFormatClock:
formatter.dateFormat = @"h:mm a";
break;
case FLEXDateFormatPreciseClock:
formatter.dateFormat = @"h:mm:ss a";
break;
case FLEXDateFormatVerbose:
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
break;
}
return [formatter stringFromDate:date];
}
@end
@@ -19,6 +19,7 @@ extern NSString * const kFLEXDefaultsHideVariablePreviewsKey;
extern NSString * const kFLEXDefaultsNetworkObserverEnabledKey;
extern NSString * const kFLEXDefaultsNetworkHostDenylistKey;
extern NSString * const kFLEXDefaultsDisableOSLogForceASLKey;
extern NSString * const kFLEXDefaultsAPNSCaptureEnabledKey;
extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
/// All BOOL preferences are NO by default
@@ -35,10 +36,15 @@ extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
/// Whether or not to register the object explorer as a JSON viewer on launch
@property (nonatomic) BOOL flex_registerDictionaryJSONViewerOnLaunch;
/// The last selected screen in the network observer
@property (nonatomic) NSInteger flex_lastNetworkObserverMode;
/// Disable os_log and re-enable ASL. May break Console.app output.
@property (nonatomic) BOOL flex_disableOSLog;
@property (nonatomic) BOOL flex_cacheOSLogMessages;
@property (nonatomic) BOOL flex_enableAPNSCapture;
@property (nonatomic) BOOL flex_explorerHidesPropertyIvars;
@property (nonatomic) BOOL flex_explorerHidesPropertyMethods;
@property (nonatomic) BOOL flex_explorerHidesPrivateMethods;
@@ -16,8 +16,10 @@ NSString * const kFLEXDefaultsHidePrivateMethodsKey = @"com.flipboard.FLEX.hide_
NSString * const kFLEXDefaultsShowMethodOverridesKey = @"com.flipboard.FLEX.show_method_overrides";
NSString * const kFLEXDefaultsHideVariablePreviewsKey = @"com.flipboard.FLEX.hide_variable_previews";
NSString * const kFLEXDefaultsNetworkObserverEnabledKey = @"com.flex.FLEXNetworkObserver.enableOnLaunch";
NSString * const kFLEXDefaultsNetworkObserverLastModeKey = @"com.flex.FLEXNetworkObserver.lastMode";
NSString * const kFLEXDefaultsNetworkHostDenylistKey = @"com.flipboard.FLEX.network_host_denylist";
NSString * const kFLEXDefaultsDisableOSLogForceASLKey = @"com.flipboard.FLEX.try_disable_os_log";
NSString * const kFLEXDefaultsAPNSCaptureEnabledKey = @"com.flipboard.FLEX.capture_apns";
NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.view_json_as_object";
#define FLEXDefaultsPathForFile(name) ({ \
@@ -92,6 +94,14 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
[self setBool:enable forKey:kFLEXDefaultsRegisterJSONExplorerKey];
}
- (NSInteger)flex_lastNetworkObserverMode {
return [self integerForKey:kFLEXDefaultsNetworkObserverLastModeKey];
}
- (void)setFlex_lastNetworkObserverMode:(NSInteger)mode {
[self setInteger:mode forKey:kFLEXDefaultsNetworkObserverLastModeKey];
}
#pragma mark System Log
- (BOOL)flex_disableOSLog {
@@ -114,6 +124,16 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
];
}
#pragma mark Push Notifications
- (BOOL)flex_enableAPNSCapture {
return [self boolForKey:kFLEXDefaultsAPNSCaptureEnabledKey];
}
- (void)setFlex_enableAPNSCapture:(BOOL)enable {
[self setBool:enable forKey:kFLEXDefaultsAPNSCaptureEnabledKey];
}
#pragma mark Object Explorer
- (BOOL)flex_explorerHidesPropertyIvars {
+7 -1
View File
@@ -28,6 +28,9 @@ typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionHandler)(void(^handler)(NSAr
/// Shows a simple alert with one button which says "Dismiss"
+ (void)showAlert:(NSString * _Nullable)title message:(NSString * _Nullable)message from:(UIViewController *)viewController;
/// Shows a simple alert with no buttons and only a title, for half a second
+ (void)showQuickAlert:(NSString *)title from:(UIViewController *)viewController;
/// Construct and display an alert
+ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
/// Construct and display an action sheet-style alert
@@ -68,8 +71,11 @@ typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionHandler)(void(^handler)(NSAr
@property (nonatomic, readonly) FLEXAlertActionStringProperty title;
/// Make the action destructive. It appears with red text.
@property (nonatomic, readonly) FLEXAlertActionProperty destructiveStyle;
/// Make the action cancel-style. It appears with a bolder font.
/// Make the action cancel-style. It sometimes appears with a bolder font.
@property (nonatomic, readonly) FLEXAlertActionProperty cancelStyle;
/// Make the action the preferred action. It appears with a bolder font.
/// The first action that is set as preferred will be used as the preferred action.
@property (nonatomic, readonly) FLEXAlertActionProperty preferred;
/// Enable or disable the action. Enabled by default.
@property (nonatomic, readonly) FLEXAlertActionBOOLProperty enabled;
/// Give the button an action. The action takes an array of text field strings.
+33 -1
View File
@@ -22,6 +22,7 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
@property (nonatomic) NSString *_title;
@property (nonatomic) UIAlertActionStyle _style;
@property (nonatomic) BOOL _disable;
@property (nonatomic) BOOL _isPreferred;
@property (nonatomic) void(^_handler)(UIAlertAction *action);
@property (nonatomic) UIAlertAction *_action;
@end
@@ -34,6 +35,18 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
} showFrom:viewController];
}
+ (void)showQuickAlert:(NSString *)title from:(UIViewController *)viewController {
UIAlertController *alert = [self makeAlert:^(FLEXAlert *make) {
make.title(title);
}];
[viewController presentViewController:alert animated:YES completion:^{
flex_dispatch_after(0.5, dispatch_get_main_queue(), ^{
[alert dismissViewControllerAnimated:YES completion:nil];
});
}];
}
#pragma mark Initialization
- (instancetype)initWithController:(UIAlertController *)controller {
@@ -60,7 +73,18 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
[alert._controller addAction:builder.action];
}
return alert._controller;
UIAlertController *controller = alert._controller;
// Set preferred action on alert controller
for (FLEXAlertAction *builder in alert._actions) {
UIAlertAction *action = builder.action;
if (builder._isPreferred) {
controller.preferredAction = action;
break;
}
}
return controller;
}
+ (void)make:(FLEXAlertBuilder)block
@@ -191,6 +215,14 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
};
}
- (FLEXAlertActionProperty)preferred {
return ^FLEXAlertAction *() {
FLEXAlertActionMutationAssertion();
self._isPreferred = YES;
return self;
};
}
- (FLEXAlertActionBOOLProperty)enabled {
return ^FLEXAlertAction *(BOOL enabled) {
FLEXAlertActionMutationAssertion();
+31 -2
View File
@@ -9,17 +9,46 @@
#import <Foundation/Foundation.h>
@class FLEXObjectRef;
NS_ASSUME_NONNULL_BEGIN
typedef void (^flex_object_enumeration_block_t)(__unsafe_unretained id object, __unsafe_unretained Class actualClass);
/// Counts and identifies all class instances on the heap.
@interface FLEXHeapSnapshot : NSObject
/// The names of every class instance discovered on the heap.
@property (nonatomic, readonly) NSArray<NSString *> *classNames;
/// A mapping of instance counts to class names.
@property (nonatomic, readonly) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
/// A mapping of class instance size to class name.
///
/// To roughly calculate the memory usage of an entire class, multiply this number by the instance count.
@property (nonatomic, readonly) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@end
@interface FLEXHeapEnumerator : NSObject
+ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)block;
/// Use carefully; this method puts a global lock on the heap in between callbacks.
///
/// Inspired by:
/// [heap_find.cpp](https://llvm.org/svn/llvm-project/lldb/tags/RELEASE_34/final/examples/darwin/heap_find/heap/heap_find.cpp)
/// and [samdmarshall](https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396)
+ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)callback
NS_SWIFT_UNAVAILABLE("Use one of the other methods instead.");
/// Returned references are not validated beyond containing a valid isa.
/// To validate them yourself, pass each reference's object to \c FLEXPointerIsValidObjcObject
+ (NSArray<FLEXObjectRef *> *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className;
/// Returned references have been validated via \c FLEXPointerIsValidObjcObject
/// @param object the object to find references to
/// @param retain whether to retain the objects referencing \c object
+ (NSArray<FLEXObjectRef *> *)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
/// Capture all live objects on the heap and do with this information what you will.
+ (FLEXHeapSnapshot *)generateHeapSnapshot;
@end
NS_ASSUME_NONNULL_END
+56 -6
View File
@@ -22,6 +22,18 @@ typedef struct {
Class isa;
} flex_maybe_object_t;
@implementation FLEXHeapSnapshot
+ (instancetype)snapshotWithCounts:(NSDictionary<NSString *, NSNumber *> *)counts
sizes:(NSDictionary<NSString *, NSNumber *> *)sizes {
FLEXHeapSnapshot *snapshot = [FLEXHeapSnapshot new];
snapshot->_classNames = counts.allKeys;
snapshot->_instanceCountsForClassNames = counts;
snapshot->_instanceSizesForClassNames = sizes;
return snapshot;
}
@end
@implementation FLEXHeapEnumerator
static void range_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount) {
@@ -138,12 +150,6 @@ static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_add
return references;
}
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className {
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
return references;
}
+ (NSArray<FLEXObjectRef *> *)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
@@ -185,4 +191,48 @@ static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_add
return instances;
}
+ (FLEXHeapSnapshot *)generateHeapSnapshot {
// Set up a CFMutableDictionary with class pointer keys and NSUInteger values.
// We abuse CFMutableDictionary a little to have primitive keys through judicious casting, but it gets the job done.
// The dictionary is intialized with a 0 count for each class so that it doesn't have to expand during enumeration.
// While it might be a little cleaner to populate an NSMutableDictionary with class name string keys to NSNumber
// counts, we choose the CF/primitives approach because it lets us enumerate the objects in the heap without
// allocating any memory during enumeration. The alternative of creating one NSString/NSNumber per object
// on the heap ends up polluting the count of live objects quite a bit.
unsigned int classCount = 0;
Class *classes = objc_copyClassList(&classCount);
CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, classCount, NULL, NULL);
for (unsigned int i = 0; i < classCount; i++) {
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)classes[i], (const void *)0);
}
// Enumerate all objects on the heap to build the counts of instances for each class
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class cls) {
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(
mutableCountsForClasses, (__bridge const void *)cls
);
instanceCount++;
CFDictionarySetValue(
mutableCountsForClasses, (__bridge const void *)cls, (const void *)instanceCount
);
}];
// Convert our CF primitive dictionary into a nicer mapping of class name strings to instance counts
NSMutableDictionary<NSString *, NSNumber *> *countsForClassNames = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSNumber *> *sizesForClassNames = [NSMutableDictionary new];
for (unsigned int i = 0; i < classCount; i++) {
Class class = classes[i];
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
NSString *className = @(class_getName(class));
if (instanceCount > 0) {
countsForClassNames[className] = @(instanceCount);
sizesForClassNames[className] = @(class_getInstanceSize(class));
}
}
free(classes);
return [FLEXHeapSnapshot snapshotWithCounts:countsForClassNames sizes:sizesForClassNames];
}
@end
+5
View File
@@ -9,6 +9,11 @@
#ifndef FLEXMacros_h
#define FLEXMacros_h
#ifndef __cplusplus
#ifndef auto
#define auto __auto_type
#endif
#endif
#define flex_keywordify class NSObject;
#define ctor flex_keywordify __attribute__((constructor)) void __flex_ctor_##__LINE__()
+2 -1
View File
@@ -51,12 +51,13 @@
+ (NSString *)prettyJSONStringFromData:(NSData *)data;
+ (BOOL)isValidJSONData:(NSData *)data;
+ (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData;
+ (BOOL)hasCompressedContentEncoding:(NSURLRequest *)request;
// Swizzling utilities
+ (SEL)swizzledSelectorForSelector:(SEL)selector;
+ (BOOL)instanceRespondsButDoesNotImplementSelector:(SEL)selector class:(Class)cls;
+ (void)replaceImplementationOfKnownSelector:(SEL)originalSelector onClass:(Class)class withBlock:(id)block swizzledSelector:(SEL)swizzledSelector;
+ (void)replaceImplementationOfKnownSelector:(SEL)originalSelector onClass:(Class)cls withBlock:(id)block swizzledSelector:(SEL)swizzledSelector;
+ (void)replaceImplementationOfSelector:(SEL)selector withSelector:(SEL)swizzledSelector forClass:(Class)cls withMethodDescription:(struct objc_method_description)methodDescription implementationBlock:(id)implementationBlock undefinedBlock:(id)undefinedBlock;
@end
+9 -3
View File
@@ -14,7 +14,7 @@
#import <objc/runtime.h>
#import <zlib.h>
BOOL FLEXConstructorsShouldRun() {
BOOL FLEXConstructorsShouldRun(void) {
#if FLEX_DISABLE_CTORS
return NO;
#else
@@ -100,8 +100,9 @@ BOOL FLEXConstructorsShouldRun() {
description = [description stringByAppendingFormat:@" %@", [self stringForCGRect:view.frame]];
}
if (view.accessibilityLabel.length > 0) {
description = [description stringByAppendingFormat:@" · %@", view.accessibilityLabel];
if (view.accessibilityLabel.length > 0 || view.accessibilityIdentifier.length > 0) {
description = [description stringByAppendingFormat:@" · %@",
view.accessibilityLabel.length > 0 ? view.accessibilityLabel : view.accessibilityIdentifier];
}
return description;
@@ -412,6 +413,11 @@ BOOL FLEXConstructorsShouldRun() {
return inflatedData;
}
+ (BOOL)hasCompressedContentEncoding:(NSURLRequest *)request {
NSString *contentEncoding = [request valueForHTTPHeaderField:@"Content-Encoding"];
return ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0);
}
+ (NSArray<UIWindow *> *)allWindows {
BOOL includeInternalWindows = YES;
BOOL onlyVisibleWindows = NO;
@@ -29,7 +29,7 @@
self.textView.backgroundColor = UIColor.blackColor;
self.textView.textColor = UIColor.whiteColor;
self.textView.font = [UIFont boldSystemFontOfSize:14.0];
self.navigationController.navigationBar.barStyle = UIBarStyleBlackOpaque;
self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
self.title = @"Simulator Shortcuts";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
+12 -4
View File
@@ -7,6 +7,7 @@
//
#import "FLEXRuntimeConstants.h"
@class FLEXObjectRef;
#define PropertyKey(suffix) kFLEXPropertyAttributeKey##suffix : @""
#define PropertyKeyGetter(getter) kFLEXPropertyAttributeKeyCustomGetter : NSStringFromSelector(@selector(getter))
@@ -45,7 +46,9 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
@interface FLEXRuntimeUtility : NSObject
// General Helpers
#pragma mark - General Helpers
/// Calls into \c FLEXPointerIsValidObjcObject()
+ (BOOL)pointerIsValidObjcObject:(const void *)pointer;
/// Unwraps raw pointers to objects stored in NSValue, and re-boxes C strings into NSStrings.
+ (id)potentiallyUnwrapBoxedPointer:(id)returnedObjectOrNil type:(const FLEXTypeEncoding *)returnType;
@@ -59,6 +62,8 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
/// @return The class hierarchy for the given object or class,
/// from the current class to the root-most class.
+ (NSArray<Class> *)classHierarchyOfObject:(id)objectOrClass;
/// @return Every subclass of the given class name.
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className;
/// Used to describe an object in brief within an explorer row
+ (NSString *)summaryForObject:(id)value;
@@ -69,16 +74,19 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
+ (BOOL)safeObject:(id)object isKindOfClass:(Class)cls;
+ (BOOL)safeObject:(id)object respondsToSelector:(SEL)sel;
// Property Helpers
#pragma mark - Property Helpers
+ (BOOL)tryAddPropertyWithName:(const char *)name
attributes:(NSDictionary<NSString *, NSString *> *)attributePairs
toClass:(__unsafe_unretained Class)theClass;
+ (NSArray<NSString *> *)allPropertyAttributeKeys;
// Method Helpers
#pragma mark - Method Helpers
+ (NSArray *)prettyArgumentComponentsForMethod:(Method)method;
// Method Calling/Field Editing
#pragma mark - Method Calling/Field Editing
+ (id)performSelector:(SEL)selector onObject:(id)object;
+ (id)performSelector:(SEL)selector
onObject:(id)object
@@ -9,6 +9,8 @@
#import <UIKit/UIKit.h>
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcInternal.h"
#import "FLEXObjectRef.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXTypeEncodingParser.h"
#import "FLEXMethod.h"
@@ -92,6 +94,12 @@ NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain
return superClasses;
}
+ (NSArray<FLEXObjectRef *> *)subclassesOfClassWithName:(NSString *)className {
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
return references;
}
+ (NSString *)safeClassNameForObject:(id)object {
// Don't assume that we have an NSObject subclass
if ([self safeObject:object respondsToSelector:@selector(class)]) {
@@ -160,6 +168,7 @@ NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain
// Single line display - replace newlines and tabs with spaces.
description = [[self safeDescriptionForObject:value] stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
description = [description stringByReplacingOccurrencesOfString:@"\t" withString:@" "];
description = [description stringByReplacingOccurrencesOfString:@" " withString:@" "];
} @catch (NSException *e) {
description = [@"Thrown: " stringByAppendingString:e.reason ?: @"(nil exception reason)"];
}
@@ -59,6 +59,7 @@ NS_INLINE BOOL flex_isTaggedPointer(const void *ptr) {
#define FLEXPointerIsTaggedPointer(obj) flex_isTaggedPointer((__bridge void *)obj)
/// Whether the given pointer is a valid, readable address.
BOOL FLEXPointerIsReadable(const void * ptr);
/// @brief Assumes memory is valid and readable.
@@ -19,7 +19,7 @@ CFSetRef FLEXKnownUnsafeIvars = nil;
(class_getInstanceVariable([cls class], name) ?: (void *)kCFNull)
__attribute__((constructor))
static void FLEXRuntimeSafteyInit() {
static void FLEXRuntimeSafteyInit(void) {
FLEXKnownUnsafeClasses = CFSetCreate(
kCFAllocatorDefault,
(const void **)(uintptr_t)FLEXKnownUnsafeClassList(),
@@ -39,7 +39,7 @@ static void FLEXRuntimeSafteyInit() {
);
}
const Class * FLEXKnownUnsafeClassList() {
const Class * FLEXKnownUnsafeClassList(void) {
if (!_UnsafeClasses) {
const Class ignored[] = {
FLEXClassPointerOrCFNull(@"__ARCLite__"),
@@ -74,7 +74,7 @@ const Class * FLEXKnownUnsafeClassList() {
return _UnsafeClasses;
}
NSSet * FLEXKnownUnsafeClassNames() {
NSSet * FLEXKnownUnsafeClassNames(void) {
static NSSet *set = nil;
if (!set) {
NSArray *ignored = @[
@@ -85,8 +85,8 @@ extern "C" BOOL FLEXIsSwiftObjectOrClass(id objOrClass) {
class_data_bits_t rodata = ((__bridge objc_class_ *)(cls))->bits;
if (@available(macOS 10.14.4, iOS 12.2, tvOS 12.2, watchOS 5.2, *)) {
return rodata & FAST_IS_SWIFT_STABLE;
return (rodata & FAST_IS_SWIFT_STABLE) != 0;
} else {
return rodata & FAST_IS_SWIFT_LEGACY;
return (rodata & FAST_IS_SWIFT_LEGACY) != 0;
}
}
@@ -0,0 +1,30 @@
//
// FLEXMetadataExtras.h
// FLEX
//
// Created by Tanner Bennett on 4/26/22.
//
#import <Foundation/Foundation.h>
#import "FLEXMethodBase.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
NS_ASSUME_NONNULL_BEGIN
/// A dictionary mapping type encoding strings to an array of field titles
extern NSString * const FLEXAuxiliarynfoKeyFieldLabels;
@protocol FLEXMetadataAuxiliaryInfo <NSObject>
/// Used to supply arbitrary additional data that need not be exposed by their own properties
- (nullable id)auxiliaryInfoForKey:(NSString *)key;
@end
@interface FLEXMethodBase (Auxiliary) <FLEXMetadataAuxiliaryInfo> @end
@interface FLEXProperty (Auxiliary) <FLEXMetadataAuxiliaryInfo> @end
@interface FLEXIvar (Auxiliary) <FLEXMetadataAuxiliaryInfo> @end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,22 @@
//
// FLEXMetadataExtras.m
// FLEX
//
// Created by Tanner Bennett on 4/26/22.
//
#import "FLEXMetadataExtras.h"
NSString * const FLEXAuxiliarynfoKeyFieldLabels = @"FLEXAuxiliarynfoKeyFieldLabels";
@implementation FLEXMethodBase (Auxiliary)
- (id)auxiliaryInfoForKey:(NSString *)key { return nil; }
@end
@implementation FLEXProperty (Auxiliary)
- (id)auxiliaryInfoForKey:(NSString *)key { return nil; }
@end
@implementation FLEXIvar (Auxiliary)
- (id)auxiliaryInfoForKey:(NSString *)key { return nil; }
@end
@@ -23,7 +23,7 @@
NSString *_flex_description;
}
/// Constructs and returns an \c FLEXSimpleMethod instance with the given name, type encoding, and implementation.
/// Constructs and returns a \c FLEXSimpleMethod instance with the given name, type encoding, and implementation.
+ (instancetype)buildMethodNamed:(NSString *)name withTypes:(NSString *)typeEncoding implementation:(IMP)implementation;
/// The selector of the method.
@@ -52,9 +52,15 @@
}
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@ name=%@, %lu properties, %lu required methods, %lu optional methods, %lu protocols>",
if (@available(iOS 10.0, *)) {
return [NSString stringWithFormat:@"<%@ name=%@, %lu required properties, %lu optional properties %lu required methods, %lu optional methods, %lu protocols>",
NSStringFromClass(self.class), self.name, (unsigned long)self.requiredProperties.count, (unsigned long)self.optionalProperties.count,
(unsigned long)self.requiredMethods.count, (unsigned long)self.optionalMethods.count, (unsigned long)self.protocols.count];
} else {
return [NSString stringWithFormat:@"<%@ name=%@, %lu properties, %lu required methods, %lu optional methods, %lu protocols>",
NSStringFromClass(self.class), self.name, (unsigned long)self.properties.count,
(unsigned long)self.requiredMethods.count, (unsigned long)self.optionalMethods.count, (unsigned long)self.protocols.count];
}
}
- (void)examine {
@@ -7,6 +7,7 @@
//
#import "FLEXImagePreviewViewController.h"
#import "FLEXActivityViewController.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "FLEXResources.h"
@@ -50,6 +51,10 @@
return self;
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
[super dismissViewControllerAnimated:flag completion:completion];
}
#pragma mark Lifecycle
@@ -128,7 +133,7 @@
}
});
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:@[self.image] applicationActivities:@[]];
UIViewController *activityVC = [FLEXActivityViewController sharing:@[self.image] source:sender];
if (!canSaveToCameraRoll && !didShowWarning) {
didShowWarning = YES;

Some files were not shown because too many files have changed in this diff Show More