You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

400 lines
16 KiB

/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRAuthAppDelegateProxy.h"
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
/** @var kProxyEnabledBundleKey
@brief The key in application's bundle plist for whether or not proxy should be enabled.
@remarks This key is a shared constant with Analytics and FCM.
*/
static NSString *const kProxyEnabledBundleKey = @"FirebaseAppDelegateProxyEnabled";
/** @fn noop
@brief A function that does nothing.
@remarks This is used as the placeholder for unimplemented UApplicationDelegate methods,
because once we added a method there is no way to remove it from the class.
*/
#if !OBJC_OLD_DISPATCH_PROTOTYPES
static void noop(void) {
}
#else
static id noop(id object, SEL cmd, ...) {
return nil;
}
#endif
/** @fn isIOS9orLater
@brief Checks whether the iOS version is 9 or later.
@returns Whether the iOS version is 9 or later.
*/
static BOOL isIOS9orLater() {
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 9.0, *)) {
return YES;
}
return NO;
#else
// UIApplicationOpenURLOptionsAnnotationKey is only available on iOS 9+.
return &UIApplicationOpenURLOptionsAnnotationKey != NULL;
#endif
}
@implementation FIRAuthAppDelegateProxy {
/** @var _appDelegate
@brief The application delegate whose method is being swizzled.
*/
id<UIApplicationDelegate> _appDelegate;
/** @var _orginalImplementationsBySelector
@brief A map from selectors to original implementations that have been swizzled.
*/
NSMutableDictionary<NSValue *, NSValue *> *_originalImplementationsBySelector;
/** @var _handlers
@brief The array of weak pointers of `id<FIRAuthAppDelegateHandler>`.
*/
NSPointerArray *_handlers;
}
- (nullable instancetype)initWithApplication:(nullable UIApplication *)application {
self = [super init];
if (self) {
id proxyEnabled = [[NSBundle mainBundle] objectForInfoDictionaryKey:kProxyEnabledBundleKey];
if ([proxyEnabled isKindOfClass:[NSNumber class]] && !((NSNumber *)proxyEnabled).boolValue) {
return nil;
}
_appDelegate = application.delegate;
if (![_appDelegate conformsToProtocol:@protocol(UIApplicationDelegate)]) {
return nil;
}
_originalImplementationsBySelector = [[NSMutableDictionary<NSValue *, NSValue *> alloc] init];
_handlers = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
// Swizzle the methods.
__weak FIRAuthAppDelegateProxy *weakSelf = self;
SEL registerDeviceTokenSelector =
@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
[self replaceSelector:registerDeviceTokenSelector
withBlock:^(id object, UIApplication* application, NSData *deviceToken) {
[weakSelf object:object
selector:registerDeviceTokenSelector
application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}];
SEL failToRegisterRemoteNotificationSelector =
@selector(application:didFailToRegisterForRemoteNotificationsWithError:);
[self replaceSelector:failToRegisterRemoteNotificationSelector
withBlock:^(id object, UIApplication* application, NSError *error) {
[weakSelf object:object
selector:failToRegisterRemoteNotificationSelector
application:application
didFailToRegisterForRemoteNotificationsWithError:error];
}];
SEL receiveNotificationSelector = @selector(application:didReceiveRemoteNotification:);
SEL receiveNotificationWithHandlerSelector =
@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
if ([_appDelegate respondsToSelector:receiveNotificationWithHandlerSelector] ||
![_appDelegate respondsToSelector:receiveNotificationSelector]) {
// Replace the modern selector which is available on iOS 7 and above.
[self replaceSelector:receiveNotificationWithHandlerSelector
withBlock:^(id object, UIApplication *application, NSDictionary *notification,
void (^completionHandler)(UIBackgroundFetchResult)) {
[weakSelf object:object
selector:receiveNotificationWithHandlerSelector
application:application
didReceiveRemoteNotification:notification
fetchCompletionHandler:completionHandler];
}];
} else {
// Replace the deprecated selector because this is the only one that the client app uses.
[self replaceSelector:receiveNotificationSelector
withBlock:^(id object, UIApplication *application, NSDictionary *notification) {
[weakSelf object:object
selector:receiveNotificationSelector
application:application
didReceiveRemoteNotification:notification];
}];
}
SEL openURLOptionsSelector = @selector(application:openURL:options:);
SEL openURLAnnotationSelector = @selector(application:openURL:sourceApplication:annotation:);
SEL handleOpenURLSelector = @selector(application:handleOpenURL:);
if (isIOS9orLater() &&
([_appDelegate respondsToSelector:openURLOptionsSelector] ||
(![_appDelegate respondsToSelector:openURLAnnotationSelector] &&
![_appDelegate respondsToSelector:handleOpenURLSelector]))) {
// Replace the modern selector which is avaliable on iOS 9 and above because this is the one
// that the client app uses or the client app doesn't use any of them.
[self replaceSelector:openURLOptionsSelector
withBlock:^BOOL(id object, UIApplication *application, NSURL *url,
NSDictionary *options) {
return [weakSelf object:object
selector:openURLOptionsSelector
application:application
openURL:url
options:options];
}];
} else if ([_appDelegate respondsToSelector:openURLAnnotationSelector] ||
![_appDelegate respondsToSelector:handleOpenURLSelector]) {
// Replace the longer form of the deprecated selectors on iOS 8 and below because this is the
// one that the client app uses or the client app doesn't use either of the applicable ones.
[self replaceSelector:openURLAnnotationSelector
withBlock:^(id object, UIApplication *application, NSURL *url,
NSString *sourceApplication, id annotation) {
return [weakSelf object:object
selector:openURLAnnotationSelector
application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}];
} else {
// Replace the shorter form of the deprecated selectors on iOS 8 and below because this is
// the only one that the client app uses.
[self replaceSelector:handleOpenURLSelector
withBlock:^(id object, UIApplication *application, NSURL *url) {
return [weakSelf object:object
selector:handleOpenURLSelector
application:application
handleOpenURL:url];
}];
}
// Reset the application delegate to clear the system cache that indicates whether each of the
// openURL: methods is implemented on the application delegate.
application.delegate = nil;
application.delegate = _appDelegate;
}
return self;
}
- (void)dealloc {
for (NSValue *selector in _originalImplementationsBySelector) {
IMP implementation = _originalImplementationsBySelector[selector].pointerValue;
Method method = class_getInstanceMethod([_appDelegate class], selector.pointerValue);
imp_removeBlock(method_setImplementation(method, implementation));
}
}
- (void)addHandler:(__weak id<FIRAuthAppDelegateHandler>)handler {
@synchronized (_handlers) {
[_handlers addPointer:(__bridge void *)handler];
}
}
+ (nullable instancetype)sharedInstance {
static dispatch_once_t onceToken;
static FIRAuthAppDelegateProxy *_Nullable sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] initWithApplication:[UIApplication sharedApplication]];
});
return sharedInstance;
}
#pragma mark - UIApplicationDelegate proxy methods.
- (void)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
if (object == _appDelegate) {
for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
[handler setAPNSToken:deviceToken];
}
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef void (*Implmentation)(id, SEL, UIApplication*, NSData *);
((Implmentation)originalImplementation)(object, selector, application, deviceToken);
}
}
- (void)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
if (object == _appDelegate) {
for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
[handler handleAPNSTokenError:error];
}
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef void (*Implmentation)(id, SEL, UIApplication *, NSError *);
((Implmentation)originalImplementation)(object, selector, application, error);
}
}
- (void)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)notification
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
if (object == _appDelegate) {
for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
if ([handler canHandleNotification:notification]) {
completionHandler(UIBackgroundFetchResultNoData);
return;
};
}
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *,
void (^)(UIBackgroundFetchResult));
((Implmentation)originalImplementation)(object, selector, application, notification,
completionHandler);
}
}
- (void)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)notification {
if (object == _appDelegate) {
for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
if ([handler canHandleNotification:notification]) {
return;
};
}
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef void (*Implmentation)(id, SEL, UIApplication*, NSDictionary *);
((Implmentation)originalImplementation)(object, selector, application, notification);
}
}
- (BOOL)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary *)options {
if (object == _appDelegate && [self delegateCanHandleURL:url]) {
return YES;
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef BOOL (*Implmentation)(id, SEL, UIApplication*, NSURL *, NSDictionary *);
return ((Implmentation)originalImplementation)(object, selector, application, url, options);
}
return NO;
}
- (BOOL)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
if (object == _appDelegate && [self delegateCanHandleURL:url]) {
return YES;
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef BOOL (*Implmentation)(id, SEL, UIApplication*, NSURL *, NSString *, id);
return ((Implmentation)originalImplementation)(object, selector, application, url,
sourceApplication, annotation);
}
return NO;
}
- (BOOL)object:(id)object
selector:(SEL)selector
application:(UIApplication *)application
handleOpenURL:(NSURL *)url {
if (object == _appDelegate && [self delegateCanHandleURL:url]) {
return YES;
}
IMP originalImplementation = [self originalImplementationForSelector:selector];
if (originalImplementation && originalImplementation != &noop) {
typedef BOOL (*Implmentation)(id, SEL, UIApplication*, NSURL *);
return ((Implmentation)originalImplementation)(object, selector, application, url);
}
return NO;
}
#pragma mark - Internal Methods
/** @fn delegateCanHandleURL:
@brief Checks for whether any of the delegates can handle the URL.
@param url The URL in question.
@return Whether any of the delegate can handle the URL.
*/
- (BOOL)delegateCanHandleURL:(NSURL *)url {
for (id<FIRAuthAppDelegateHandler> handler in [self handlers]) {
if ([handler canHandleURL:url]) {
return YES;
};
}
return NO;
}
/** @fn handlers
@brief Gets the list of handlers from `_handlers` safely.
*/
- (NSArray<id<FIRAuthAppDelegateHandler>> *)handlers {
@synchronized (_handlers) {
NSMutableArray<id<FIRAuthAppDelegateHandler>> *liveHandlers =
[[NSMutableArray<id<FIRAuthAppDelegateHandler>> alloc] initWithCapacity:_handlers.count];
for (__weak id<FIRAuthAppDelegateHandler> handler in _handlers) {
if (handler) {
[liveHandlers addObject:handler];
}
}
if (liveHandlers.count < _handlers.count) {
[_handlers compact];
}
return liveHandlers;
}
}
/** @fn replaceSelector:withBlock:
@brief replaces the implementation for a method of `_appDelegate` specified by a selector.
@param selector The selector for the method.
@param block The block as the new implementation of the method.
*/
- (void)replaceSelector:(SEL)selector withBlock:(id)block {
Method originalMethod = class_getInstanceMethod([_appDelegate class], selector);
IMP newImplementation = imp_implementationWithBlock(block);
IMP originalImplementation;
if (originalMethod) {
originalImplementation = method_setImplementation(originalMethod, newImplementation) ?: &noop;
} else {
// The original method was not implemented in the class, add it with the new implementation.
struct objc_method_description methodDescription =
protocol_getMethodDescription(@protocol(UIApplicationDelegate), selector, NO, YES);
class_addMethod([_appDelegate class], selector, newImplementation, methodDescription.types);
originalImplementation = &noop;
}
_originalImplementationsBySelector[[NSValue valueWithPointer:selector]] =
[NSValue valueWithPointer:originalImplementation];
}
/** @fn originalImplementationForSelector:
@brief Gets the original implementation for the given selector.
@param selector The selector for the method that has been replaced.
@return The original implementation if there was one.
*/
- (IMP)originalImplementationForSelector:(SEL)selector {
return _originalImplementationsBySelector[[NSValue valueWithPointer:selector]].pointerValue;
}
@end
NS_ASSUME_NONNULL_END