diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h index 23a9528a2..2f073046b 100644 --- a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h @@ -14,18 +14,12 @@ @interface PFObjectSubclassingController : NSObject -///-------------------------------------- -#pragma mark - Init -///-------------------------------------- - -//TODO: (nlutsenko, richardross) Make it not terrible aka don't have singletons. -+ (instancetype)defaultController; -+ (void)clearDefaultController; - ///-------------------------------------- #pragma mark - Registration ///-------------------------------------- +- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe; + - (Class)subclassForParseClassName:(NSString *)parseClassName; - (void)registerSubclass:(Class)kls; - (void)unregisterSubclass:(Class)kls; diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m index 91e740d71..ba29a089d 100644 --- a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m @@ -16,6 +16,8 @@ #import "PFAssert.h" #import "PFMacros.h" +#import "PFObject.h" +#import "PFObject+Subclass.h" #import "PFObjectSubclassInfo.h" #import "PFPropertyInfo_Private.h" #import "PFPropertyInfo_Runtime.h" @@ -99,17 +101,6 @@ - (instancetype)init { return self; } -+ (instancetype)defaultController { - if (!defaultController_) { - defaultController_ = [[PFObjectSubclassingController alloc] init]; - } - return defaultController_; -} - -+ (void)clearDefaultController { - defaultController_ = nil; -} - ///-------------------------------------- #pragma mark - Public ///-------------------------------------- @@ -122,6 +113,33 @@ + (void)clearDefaultController { return result; } +- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe { + // NOTE: Potential race-condition here - if another thread dynamically loads a bundle, we may end up accidentally + // Skipping a bundle. Not entirely sure of the best solution to that here. + if (shouldSubscribe) { + @weakify(self); + [[NSNotificationCenter defaultCenter] addObserverForName:NSBundleDidLoadNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + @strongify(self); + [self _registerSubclassesInBundle:note.object]; + }]; + } + NSArray *bundles = [[NSBundle allFrameworks] arrayByAddingObjectsFromArray:[NSBundle allBundles]]; + for (NSBundle *bundle in bundles) { + // Skip bundles that aren't loaded yet. + if (!bundle.loaded || !bundle.executablePath) { + continue; + } + // Filter out any system bundles + if ([[bundle bundlePath] hasPrefix:@"/System/"] || [[bundle bundlePath] hasPrefix:@"/Library/"]) { + continue; + } + [self _registerSubclassesInBundle:bundle]; + } +} + - (void)registerSubclass:(Class)kls { pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{ [self _rawRegisterSubclass:kls]; @@ -315,4 +333,47 @@ - (void)_rawRegisterSubclass:(Class)kls { _registeredSubclasses[[kls parseClassName]] = subclassInfo; } +- (void)_registerSubclassesInBundle:(NSBundle *)bundle { + PFConsistencyAssert(bundle.loaded, @"Cannot register subclasses in a bundle that hasn't been loaded!"); + dispatch_sync(_registeredSubclassesAccessQueue, ^{ + Class pfObjectClass = [PFObject class]; + unsigned bundleClassCount = 0; + + // NSBundle's executablePath does not resolve symlinks (sadface). Unfortunately, objc_copyClassNamesForImage() + // Only takes absolute paths, as it does a direct string compare. This causes issues when using a framework + // which uses the `Versions/XXX` format, vs. just having the binary be at the root of the `.framework` bundle. + // However, we cannot use stringByResolvingSymlinksInPath for some reason here. On iOS, it never resolves the first, + // symlink in the path, e.g. /var to /private/var. + // Luckily, the POSIX function `realpath` will expand that symlink, and give us the path we need. + char pathBuf[PATH_MAX + 1] = { 0 }; + if (!realpath([[bundle executablePath] UTF8String], pathBuf)) { + return; + } + + const char **classNames = objc_copyClassNamesForImage(pathBuf, &bundleClassCount); + for (unsigned i = 0; i < bundleClassCount; i++) { + Class bundleClass = objc_getClass(classNames[i]); + // For obvious reasons, don't register the PFObject class. + if (bundleClass == pfObjectClass) { + continue; + } + // NOTE: (richardross) Cannot use isSubclassOfClass here. Some classes may be part of a system bundle (even + // though we attempt to filter those out) that may be an internal class which doesn't inherit from NSObject. + // Scary, I know! + for (Class kls = bundleClass; kls != nil; kls = class_getSuperclass(kls)) { + if (kls == pfObjectClass) { + // Do class_conformsToProtocol as late in the checking as possible, as its SUUUPER slow. + // Behind the scenes this is a strcmp (lolwut?) + if (class_conformsToProtocol(bundleClass, @protocol(PFSubclassing)) && + !class_conformsToProtocol(bundleClass, @protocol(PFSubclassingSkipAutomaticRegistration))) { + [self _rawRegisterSubclass:bundleClass]; + } + break; + } + } + } + free(classNames); + }); +} + @end diff --git a/Parse/Internal/PFCoreDataProvider.h b/Parse/Internal/PFCoreDataProvider.h index b1e0ac1b8..19ff7f068 100644 --- a/Parse/Internal/PFCoreDataProvider.h +++ b/Parse/Internal/PFCoreDataProvider.h @@ -36,6 +36,14 @@ NS_ASSUME_NONNULL_BEGIN @end +@class PFObjectSubclassingController; + +@protocol PFObjectSubclassingControllerProvider + +@property (nonatomic, strong) PFObjectSubclassingController *objectSubclassingController; + +@end + @class PFObjectBatchController; @protocol PFObjectBatchController diff --git a/Parse/Internal/PFCoreManager.h b/Parse/Internal/PFCoreManager.h index 4052db1ac..2ba1ee3bd 100644 --- a/Parse/Internal/PFCoreManager.h +++ b/Parse/Internal/PFCoreManager.h @@ -42,6 +42,7 @@ PFPersistenceControllerProvider> + +@end + NS_ASSUME_NONNULL_END diff --git a/Parse/PFObject.m b/Parse/PFObject.m index e8e23e653..a7027aced 100644 --- a/Parse/PFObject.m +++ b/Parse/PFObject.m @@ -2465,7 +2465,7 @@ + (PFCurrentUserController *)currentUserController { } + (PFObjectSubclassingController *)subclassingController { - return [PFObjectSubclassingController defaultController]; + return [Parse _currentManager].coreManager.objectSubclassingController; } @end diff --git a/Parse/Parse.m b/Parse/Parse.m index b87ab7b5c..d54fbf0d5 100644 --- a/Parse/Parse.m +++ b/Parse/Parse.m @@ -80,22 +80,6 @@ + (void)initializeWithConfiguration:(ParseClientConfiguration *)configuration { currentParseManager_ = manager; - PFObjectSubclassingController *subclassingController = [PFObjectSubclassingController defaultController]; - // Register built-in subclasses of PFObject so they get used. - // We're forced to register subclasses directly this way, in order to prevent a deadlock. - // If we ever switch to bundle scanning, this code can go away. - [subclassingController registerSubclass:[PFUser class]]; - [subclassingController registerSubclass:[PFSession class]]; - [subclassingController registerSubclass:[PFRole class]]; - [subclassingController registerSubclass:[PFPin class]]; - [subclassingController registerSubclass:[PFEventuallyPin class]]; -#if !TARGET_OS_WATCH && !TARGET_OS_TV - [subclassingController registerSubclass:[PFInstallation class]]; -#endif -#if TARGET_OS_IOS || TARGET_OS_TV - [subclassingController registerSubclass:[PFProduct class]]; -#endif - #if TARGET_OS_IOS [PFNetworkActivityIndicatorManager sharedManager].enabled = YES; #endif diff --git a/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m index d01940896..a148eb43e 100644 --- a/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m +++ b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m @@ -42,7 +42,6 @@ - (void)setUp { - (void)tearDown { [[Parse _currentManager] clearEventuallyQueue]; [Parse _clearCurrentManager]; - [PFObjectSubclassingController clearDefaultController]; [super tearDown]; } diff --git a/Tests/Unit/ACLTests.m b/Tests/Unit/ACLTests.m index 6089bf3ad..43eb237c4 100644 --- a/Tests/Unit/ACLTests.m +++ b/Tests/Unit/ACLTests.m @@ -13,10 +13,10 @@ #import "PFMacros.h" #import "PFObjectPrivate.h" #import "PFRole.h" -#import "PFTestCase.h" +#import "PFUnitTestCase.h" #import "PFUserPrivate.h" -@interface ACLTests : PFTestCase +@interface ACLTests : PFUnitTestCase @end @@ -210,8 +210,6 @@ - (void)testUnsharedCopy { - (void)testACLRequiresObjectId { - [PFUser registerSubclass]; - PFACL *acl = [PFACL ACL]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" diff --git a/Tests/Unit/ObjectSubclassPropertiesTests.m b/Tests/Unit/ObjectSubclassPropertiesTests.m index ea7bd9f76..a85d3a002 100644 --- a/Tests/Unit/ObjectSubclassPropertiesTests.m +++ b/Tests/Unit/ObjectSubclassPropertiesTests.m @@ -105,22 +105,6 @@ @interface ObjectSubclassPropertiesTests : PFUnitTestCase @implementation ObjectSubclassPropertiesTests -///-------------------------------------- -#pragma mark - XCTestCase -///-------------------------------------- - -- (void)setUp { - [super setUp]; - - [PFTestObject registerSubclass]; -} - -- (void)tearDown { - [PFObject unregisterSubclass:[PFTestObject class]]; - - [super tearDown]; -} - ///-------------------------------------- #pragma mark - Tests ///-------------------------------------- diff --git a/Tests/Unit/ObjectSubclassTests.m b/Tests/Unit/ObjectSubclassTests.m index 6ef529143..e11a7c9eb 100644 --- a/Tests/Unit/ObjectSubclassTests.m +++ b/Tests/Unit/ObjectSubclassTests.m @@ -17,7 +17,7 @@ #pragma mark - Helpers ///-------------------------------------- -@interface TheFlash : PFObject { +@interface TheFlash : PFObject { NSString *flashName; } @@ -59,7 +59,7 @@ + (NSString *)parseClassName { @end -@interface ClassWithDirtyingConstructor : PFObject +@interface ClassWithDirtyingConstructor : PFObject @end @implementation ClassWithDirtyingConstructor @@ -85,7 +85,7 @@ @interface UtilityClass : PFObject @implementation UtilityClass @end -@interface DescendantOfUtility : UtilityClass +@interface DescendantOfUtility : UtilityClass @end @implementation DescendantOfUtility @@ -94,7 +94,7 @@ + (NSString *)parseClassName { } @end -@interface StateClass : PFObject +@interface StateClass : PFObject @property (nonatomic, copy) NSString *state; @@ -120,17 +120,6 @@ @interface ObjectSubclassTests : PFUnitTestCase @implementation ObjectSubclassTests -///-------------------------------------- -#pragma mark - XCTestCase -///-------------------------------------- - -- (void)tearDown { - [PFObject unregisterSubclass:[TheFlash class]]; - [PFObject unregisterSubclass:[BarryAllen class]]; - - [super tearDown]; -} - ///-------------------------------------- #pragma mark - Tests ///-------------------------------------- @@ -173,18 +162,6 @@ - (void)testSubclassesCanInheritUtilityClassesWithoutParseClassName { [DescendantOfUtility registerSubclass]; } -- (void)testSubclassRegistrationBeforeInitializingParse { - [[Parse _currentManager] clearEventuallyQueue]; - [Parse _clearCurrentManager]; - - [TheFlash registerSubclass]; - - [Parse setApplicationId:@"a" clientKey:@"b"]; - - PFObject *theFlash = [PFObject objectWithClassName:@"Person"]; - PFAssertIsKindOfClass(theFlash, [TheFlash class]); -} - - (void)testStateIsSubclassable { [StateClass registerSubclass]; StateClass *stateClass = [StateClass object]; diff --git a/Tests/Unit/ObjectSubclassingControllerTests.m b/Tests/Unit/ObjectSubclassingControllerTests.m index e2a564931..9e4306251 100644 --- a/Tests/Unit/ObjectSubclassingControllerTests.m +++ b/Tests/Unit/ObjectSubclassingControllerTests.m @@ -16,13 +16,13 @@ #import "PFUnitTestCase.h" #import "ParseUnitTests-Swift.h" -@interface TestSubclass : PFObject +@interface TestSubclass : PFObject @end -@interface NotSubclass : PFObject +@interface NotSubclass : PFObject @end -@interface PropertySubclass : PFObject { +@interface PropertySubclass : PFObject { @public id _ivarProperty; } diff --git a/Tests/Unit/OfflineQueryLogicUnitTests.m b/Tests/Unit/OfflineQueryLogicUnitTests.m index f97e88595..03b712344 100644 --- a/Tests/Unit/OfflineQueryLogicUnitTests.m +++ b/Tests/Unit/OfflineQueryLogicUnitTests.m @@ -29,12 +29,11 @@ @implementation OfflineQueryLogicUnitTests - (void)setUp { [super setUp]; - [PFUser registerSubclass]; _user = [PFUser user]; } - (void)tearDown { - [PFObject unregisterSubclass:[PFUser class]]; + _user = nil; [super tearDown]; } diff --git a/Tests/Unit/SessionControllerTests.m b/Tests/Unit/SessionControllerTests.m index cd0bc930c..763d225dc 100644 --- a/Tests/Unit/SessionControllerTests.m +++ b/Tests/Unit/SessionControllerTests.m @@ -16,31 +16,15 @@ #import "PFObjectPrivate.h" #import "PFRESTCommand.h" #import "PFSessionController.h" -#import "PFTestCase.h" +#import "PFUnitTestCase.h" #import "Parse_Private.h" -@interface SessionControllerTests : PFTestCase +@interface SessionControllerTests : PFUnitTestCase @end @implementation SessionControllerTests -///-------------------------------------- -#pragma mark - XCTestCase -///-------------------------------------- - -- (void)setUp { - [super setUp]; - - [PFSession registerSubclass]; -} - -- (void)tearDown { - [PFObject unregisterSubclass:[PFSession class]]; - - [super tearDown]; -} - ///-------------------------------------- #pragma mark - Helpers ///-------------------------------------- diff --git a/Tests/Unit/SessionUnitTests.m b/Tests/Unit/SessionUnitTests.m index d097af3c0..f79d0800f 100644 --- a/Tests/Unit/SessionUnitTests.m +++ b/Tests/Unit/SessionUnitTests.m @@ -45,12 +45,6 @@ - (PFSessionController *)sessionControllerMockWithSessionResult:(PFSession *)ses ///-------------------------------------- - (void)testSessionClassIsRegistered { - [[Parse _currentManager] clearEventuallyQueue]; - [Parse _clearCurrentManager]; - [PFObjectSubclassingController clearDefaultController]; - - [PFObject unregisterSubclass:[PFSession class]]; - [Parse setApplicationId:@"a" clientKey:@"b"]; XCTAssertNotNil([PFSession query]); } @@ -60,8 +54,6 @@ - (void)testConstructorsClassNameValidation { } - (void)testSessionImmutableFieldsCannotBeChanged { - [PFSession registerSubclass]; - PFSession *session = [PFSession object]; session[@"yolo"] = @"El Capitan!"; // Test for regular mutability PFAssertThrowsInvalidArgumentException(session[@"sessionToken"] = @"a"); @@ -73,8 +65,6 @@ - (void)testSessionImmutableFieldsCannotBeChanged { } - (void)testSessionImmutableFieldsCannotBeDeleted { - [PFSession registerSubclass]; - PFSession *session = [PFSession object]; [session removeObjectForKey:@"yolo"];// Test for regular mutability diff --git a/Tests/Unit/UserUnitTests.m b/Tests/Unit/UserUnitTests.m index 7daf37e73..fe82e0fe7 100644 --- a/Tests/Unit/UserUnitTests.m +++ b/Tests/Unit/UserUnitTests.m @@ -25,15 +25,11 @@ - (void)testConstructorsClassNameValidation { } - (void)testImmutableFieldsCannotBeChanged { - [PFUser registerSubclass]; - PFUser *user = [PFUser object]; PFAssertThrowsInvalidArgumentException(user[@"sessionToken"] = @"a"); } - (void)testImmutableFieldsCannotBeDeleted { - [PFUser registerSubclass]; - PFUser *user = [PFUser object]; PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"username"]); PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"sessionToken"]);