/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTComponentData.h" #import #import "RCTBridge.h" #import "RCTBridgeModule.h" #import "RCTComponentEvent.h" #import "RCTConstants.h" #import "RCTConvert.h" #import "RCTEventDispatcherProtocol.h" #import "RCTParserUtils.h" #import "RCTShadowView.h" #import "RCTUtils.h" #import "UIView+React.h" typedef void (^RCTPropBlock)(id view, id json); typedef NSMutableDictionary RCTPropBlockDictionary; typedef void (^InterceptorBlock)(NSString *eventName, NSDictionary *event, id sender); /** * Get the converter function for the specified type */ static SEL selectorForType(NSString *type) { const char *input = type.UTF8String; return NSSelectorFromString([RCTParseType(&input) stringByAppendingString:@":"]); } @implementation RCTComponentData { id _defaultView; // Only needed for RCT_CUSTOM_VIEW_PROPERTY RCTPropBlockDictionary *_viewPropBlocks; RCTPropBlockDictionary *_shadowPropBlocks; __weak RCTBridge *_bridge; __weak id _eventDispatcher; } @synthesize manager = _manager; @synthesize bridgelessViewManager = _bridgelessViewManager; - (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge eventDispatcher:(id)eventDispatcher { if ((self = [super init])) { _bridge = bridge; _eventDispatcher = eventDispatcher; _managerClass = managerClass; _viewPropBlocks = [NSMutableDictionary new]; _shadowPropBlocks = [NSMutableDictionary new]; _name = moduleNameForClass(managerClass); } return self; } - (RCTViewManager *)manager { if (!_manager && _bridge) { _manager = [_bridge moduleForClass:_managerClass]; } else if (!_manager && !_bridgelessViewManager) { _bridgelessViewManager = [_managerClass new]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTDidInitializeModuleNotification object:nil userInfo:@{@"module" : _bridgelessViewManager}]; } return _manager ?: _bridgelessViewManager; } RCT_NOT_IMPLEMENTED(-(instancetype)init) - (UIView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag { RCTAssertMainQueue(); UIView *view = [self.manager view]; view.reactTag = tag; view.rootTag = rootTag; view.multipleTouchEnabled = YES; view.userInteractionEnabled = YES; // required for touch handling view.layer.allowsGroupOpacity = YES; // required for touch handling return view; } - (RCTShadowView *)createShadowViewWithTag:(NSNumber *)tag { RCTShadowView *shadowView = [self.manager shadowView]; shadowView.reactTag = tag; shadowView.viewName = _name; return shadowView; } - (void)callCustomSetter:(SEL)setter onView:(id)view withProp:(id)json isShadowView:(BOOL)isShadowView { json = RCTNilIfNull(json); if (!isShadowView) { if (!json && !_defaultView) { // Only create default view if json is null _defaultView = [self createViewWithTag:nil rootTag:nil]; } ((void (*)(id, SEL, id, id, id))objc_msgSend)(self.manager, setter, json, view, _defaultView); } else { ((void (*)(id, SEL, id, id))objc_msgSend)(self.manager, setter, json, view); } } static RCTPropBlock createEventSetter( NSString *propName, SEL setter, InterceptorBlock eventInterceptor, id eventDispatcher) { __weak id weakEventDispatcher = eventDispatcher; return ^(id target, id json) { void (^eventHandler)(NSDictionary *event) = nil; if ([RCTConvert BOOL:json]) { __weak id weakTarget = target; eventHandler = ^(NSDictionary *event) { // The component no longer exists, we shouldn't send the event id strongTarget = weakTarget; if (!strongTarget) { return; } if (eventInterceptor) { eventInterceptor(propName, event, strongTarget.reactTag); } else { RCTComponentEvent *componentEvent = [[RCTComponentEvent alloc] initWithName:propName viewTag:strongTarget.reactTag body:event]; [weakEventDispatcher sendEvent:componentEvent]; } }; } ((void (*)(id, SEL, id))objc_msgSend)(target, setter, eventHandler); }; } static RCTPropBlock createNSInvocationSetter(NSMethodSignature *typeSignature, SEL type, SEL getter, SEL setter) { NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; typeInvocation.selector = type; typeInvocation.target = [RCTConvert class]; __block NSInvocation *targetInvocation = nil; __block NSMutableData *defaultValue = nil; return ^(id target, id json) { if (!target) { return; } // Get default value if (!defaultValue) { if (!json) { // We only set the defaultValue when we first pass a non-null // value, so if the first value sent for a prop is null, it's // a no-op (we'd be resetting it to its default when its // value is already the default). return; } // Use NSMutableData to store defaultValue instead of malloc, so // it will be freed automatically when setterBlock is released. defaultValue = [[NSMutableData alloc] initWithLength:typeSignature.methodReturnLength]; if ([target respondsToSelector:getter]) { NSMethodSignature *signature = [target methodSignatureForSelector:getter]; NSInvocation *sourceInvocation = [NSInvocation invocationWithMethodSignature:signature]; sourceInvocation.selector = getter; [sourceInvocation invokeWithTarget:target]; [sourceInvocation getReturnValue:defaultValue.mutableBytes]; } } // Get value BOOL freeValueOnCompletion = NO; void *value = defaultValue.mutableBytes; if (json) { freeValueOnCompletion = YES; value = malloc(typeSignature.methodReturnLength); if (!value) { // CWE - 391 : Unchecked error condition // https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html // https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c abort(); } [typeInvocation setArgument:&json atIndex:2]; [typeInvocation invoke]; [typeInvocation getReturnValue:value]; } // Set value if (!targetInvocation) { NSMethodSignature *signature = [target methodSignatureForSelector:setter]; targetInvocation = [NSInvocation invocationWithMethodSignature:signature]; targetInvocation.selector = setter; } [targetInvocation setArgument:value atIndex:2]; [targetInvocation invokeWithTarget:target]; if (freeValueOnCompletion) { // Only free the value if we `malloc`d it locally, otherwise it // points to `defaultValue.mutableBytes`, which is managed by ARC. free(value); } }; } - (RCTPropBlock)createPropBlock:(NSString *)name isShadowView:(BOOL)isShadowView { // Get type SEL type = NULL; NSString *keyPath = nil; SEL selector = NSSelectorFromString([NSString stringWithFormat:@"propConfig%@_%@", isShadowView ? @"Shadow" : @"", name]); if ([_managerClass respondsToSelector:selector]) { NSArray *typeAndKeyPath = ((NSArray * (*)(id, SEL)) objc_msgSend)(_managerClass, selector); type = selectorForType(typeAndKeyPath[0]); keyPath = typeAndKeyPath.count > 1 ? typeAndKeyPath[1] : nil; } else { return ^(__unused id view, __unused id json) { }; } // Check for custom setter if ([keyPath isEqualToString:@"__custom__"]) { // Get custom setter. There is no default view in the shadow case, so the selector is different. NSString *selectorString; if (!isShadowView) { selectorString = [NSString stringWithFormat:@"set_%@:for%@View:withDefaultView:", name, isShadowView ? @"Shadow" : @""]; } else { selectorString = [NSString stringWithFormat:@"set_%@:forShadowView:", name]; } SEL customSetter = NSSelectorFromString(selectorString); __weak RCTComponentData *weakSelf = self; return ^(id view, id json) { [weakSelf callCustomSetter:customSetter onView:view withProp:json isShadowView:isShadowView]; }; } else { // Disect keypath NSString *key = name; NSArray *parts = [keyPath componentsSeparatedByString:@"."]; if (parts) { key = parts.lastObject; parts = [parts subarrayWithRange:(NSRange){0, parts.count - 1}]; } // Get property getter SEL getter = NSSelectorFromString(key); // Get property setter SEL setter = NSSelectorFromString( [NSString stringWithFormat:@"set%@%@:", [key substringToIndex:1].uppercaseString, [key substringFromIndex:1]]); // Build setter block void (^setterBlock)(id target, id json) = nil; if (type == NSSelectorFromString(@"RCTBubblingEventBlock:") || type == NSSelectorFromString(@"RCTDirectEventBlock:") || type == NSSelectorFromString(@"RCTCapturingEventBlock:")) { // Special case for event handlers setterBlock = createEventSetter(name, setter, self.eventInterceptor, _bridge ? _bridge.eventDispatcher : _eventDispatcher); } else { // Ordinary property handlers NSMethodSignature *typeSignature = [[RCTConvert class] methodSignatureForSelector:type]; if (!typeSignature) { RCTLogError(@"No +[RCTConvert %@] function found.", NSStringFromSelector(type)); return ^(__unused id view, __unused id json) { }; } switch (typeSignature.methodReturnType[0]) { #define RCT_CASE(_value, _type) \ case _value: { \ __block BOOL setDefaultValue = NO; \ __block _type defaultValue; \ _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ _type (*get)(id, SEL) = (typeof(get))objc_msgSend; \ void (*set)(id, SEL, _type) = (typeof(set))objc_msgSend; \ setterBlock = ^(id target, id json) { \ if (json) { \ if (!setDefaultValue && target) { \ if ([target respondsToSelector:getter]) { \ defaultValue = get(target, getter); \ } \ setDefaultValue = YES; \ } \ set(target, setter, convert([RCTConvert class], type, json)); \ } else if (setDefaultValue) { \ set(target, setter, defaultValue); \ } \ }; \ break; \ } RCT_CASE(_C_SEL, SEL) RCT_CASE(_C_CHARPTR, const char *) RCT_CASE(_C_CHR, char) RCT_CASE(_C_UCHR, unsigned char) RCT_CASE(_C_SHT, short) RCT_CASE(_C_USHT, unsigned short) RCT_CASE(_C_INT, int) RCT_CASE(_C_UINT, unsigned int) RCT_CASE(_C_LNG, long) RCT_CASE(_C_ULNG, unsigned long) RCT_CASE(_C_LNG_LNG, long long) RCT_CASE(_C_ULNG_LNG, unsigned long long) RCT_CASE(_C_FLT, float) RCT_CASE(_C_DBL, double) RCT_CASE(_C_BOOL, BOOL) RCT_CASE(_C_PTR, void *) RCT_CASE(_C_ID, id) case _C_STRUCT_B: default: { setterBlock = createNSInvocationSetter(typeSignature, type, getter, setter); break; } } } return ^(__unused id view, __unused id json) { // Follow keypath id target = view; for (NSString *part in parts) { target = [target valueForKey:part]; } // Set property with json setterBlock(target, RCTNilIfNull(json)); }; } } - (RCTPropBlock)propBlockForKey:(NSString *)name isShadowView:(BOOL)isShadowView { RCTPropBlockDictionary *propBlocks = isShadowView ? _shadowPropBlocks : _viewPropBlocks; RCTPropBlock propBlock = propBlocks[name]; if (!propBlock) { propBlock = [self createPropBlock:name isShadowView:isShadowView]; #if RCT_DEBUG // Provide more useful log feedback if there's an error RCTPropBlock unwrappedBlock = propBlock; __weak __typeof(self) weakSelf = self; propBlock = ^(id view, id json) { NSString *logPrefix = [NSString stringWithFormat:@"Error setting property '%@' of %@ with tag #%@: ", name, weakSelf.name, view.reactTag]; RCTPerformBlockWithLogPrefix( ^{ unwrappedBlock(view, json); }, logPrefix); }; #endif propBlocks[name] = [propBlock copy]; } return propBlock; } - (void)setProps:(NSDictionary *)props forView:(id)view { if (!view) { return; } [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id json, __unused BOOL *stop) { [self propBlockForKey:key isShadowView:NO](view, json); }]; } - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowView *)shadowView { if (!shadowView) { return; } [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id json, __unused BOOL *stop) { [self propBlockForKey:key isShadowView:YES](shadowView, json); }]; } - (NSDictionary *)viewConfig { NSMutableArray *bubblingEvents = [NSMutableArray new]; NSMutableArray *capturingEvents = [NSMutableArray new]; NSMutableArray *directEvents = [NSMutableArray new]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (RCTClassOverridesInstanceMethod(_managerClass, @selector(customBubblingEventTypes))) { NSArray *events = [self.manager customBubblingEventTypes]; for (NSString *event in events) { [bubblingEvents addObject:RCTNormalizeInputEventName(event)]; } } #pragma clang diagnostic pop unsigned int count = 0; NSMutableDictionary *propTypes = [NSMutableDictionary new]; Method *methods = class_copyMethodList(object_getClass(_managerClass), &count); for (unsigned int i = 0; i < count; i++) { SEL selector = method_getName(methods[i]); const char *selectorName = sel_getName(selector); if (strncmp(selectorName, "propConfig", strlen("propConfig")) != 0) { continue; } // We need to handle both propConfig_* and propConfigShadow_* methods const char *underscorePos = strchr(selectorName + strlen("propConfig"), '_'); if (!underscorePos) { continue; } NSString *name = @(underscorePos + 1); NSString *type = ((NSArray * (*)(id, SEL)) objc_msgSend)(_managerClass, selector)[0]; if (RCT_DEBUG && propTypes[name] && ![propTypes[name] isEqualToString:type]) { RCTLogError( @"Property '%@' of component '%@' redefined from '%@' " "to '%@'", name, _name, propTypes[name], type); } if ([type isEqualToString:@"RCTBubblingEventBlock"]) { [bubblingEvents addObject:RCTNormalizeInputEventName(name)]; propTypes[name] = @"BOOL"; } else if ([type isEqualToString:@"RCTCapturingEventBlock"]) { [capturingEvents addObject:RCTNormalizeInputEventName(name)]; propTypes[name] = @"BOOL"; } else if ([type isEqualToString:@"RCTDirectEventBlock"]) { [directEvents addObject:RCTNormalizeInputEventName(name)]; propTypes[name] = @"BOOL"; } else { propTypes[name] = type; } } free(methods); #if RCT_DEBUG for (NSString *event in bubblingEvents) { if ([directEvents containsObject:event]) { RCTLogError( @"Component '%@' registered '%@' as both a bubbling event " "and a direct event", _name, event); } } #endif Class superClass = [_managerClass superclass]; return @{ @"propTypes" : propTypes, @"directEvents" : directEvents, @"bubblingEvents" : bubblingEvents, @"capturingEvents" : capturingEvents, @"baseModuleName" : superClass == [NSObject class] ? (id)kCFNull : moduleNameForClass(superClass), }; } static NSString *moduleNameForClass(Class managerClass) { // Hackety hack, this partially re-implements RCTBridgeModuleNameForClass // We want to get rid of RCT and RK prefixes, but a lot of JS code still references // view names by prefix. So, while RCTBridgeModuleNameForClass now drops these // prefixes by default, we'll still keep them around here. NSString *name = [managerClass moduleName]; if (name.length == 0) { name = NSStringFromClass(managerClass); } if ([name hasPrefix:@"RK"]) { name = [name stringByReplacingCharactersInRange:(NSRange){0, @"RK".length} withString:@"RCT"]; } if ([name hasSuffix:@"Manager"]) { name = [name substringToIndex:name.length - @"Manager".length]; } RCTAssert(name.length, @"Invalid moduleName '%@'", name); return name; } @end