Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
FR: Validate and recover URL strings in IAM
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/feature_request.md
-->

### Feature proposal

* Firebase Component: IAM

URLs in IAM are constructed from plain strings, which makes them vulnerable to easy-to-make mistakes, such as trailing whitespaces. In some cases, such mistakes result in crashes (#8482).

Although it’s not possible to rule out every situation which prevents a successful string-to-URL conversion, certain issues—such as redundant whitespaces in the URL string—can be fixed automatically.

Additionally, the Firebase Console clearly states that image URLs must use the HTTPS scheme. This requirement is not enforced in the SDK, which may lead to inconsistent results, depending on App Transport Security configuration et al., and a bunch of confusion.

<img width="671" src="https://user-images.githubusercontent.com/4648742/128037494-931275f9-6ab6-4f15-9c98-321acddd4ae2.png">


Discussion / hints:

Fix patch:
diff --git a/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m b/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m
index b83facdfe23..bcb08fb3410 100644
--- a/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m
+++ b/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m
@@ -287,12 +287,10 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
       return nil;
     }
 
-    NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr];
-    NSURL *landscapeImageURL =
-        (landscapeImageURLStr.length == 0) ? nil : [NSURL URLWithString:landscapeImageURLStr];
-    NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr];
-    NSURL *secondaryActionURL =
-        (secondaryActionURLStr.length == 0) ? nil : [NSURL URLWithString:secondaryActionURLStr];
+    NSURL *imageURL = [self imageURLFromURLString:imageURLStr];
+    NSURL *landscapeImageURL = [self imageURLFromURLString:landscapeImageURLStr];
+    NSURL *actionURL = [self urlFromURLString:actionURLStr];
+    NSURL *secondaryActionURL = [self urlFromURLString:secondaryActionURLStr];
     FIRIAMRenderingEffectSetting *renderEffect =
         [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
     renderEffect.viewMode = mode;
@@ -376,6 +374,28 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
     return nil;
   }
 }
+
+- (nullable NSURL *)imageURLFromURLString:(NSString *)string {
+  NSURL *url = [self urlFromURLString:string];
+
+  // Image URLs must be valid HTTPS links, according to the Firebase Console.
+  if (![url.scheme.lowercaseString isEqualToString:@"https"]) return nil;
+
+  return url;
+}
+
+- (nullable NSURL *)urlFromURLString:(NSString *)string {
+  NSString *sanitizedString = [self sanitizedURLStringFromString:string];
+
+  if (sanitizedString.length == 0) return nil;
+
+  return [NSURL URLWithString:sanitizedString];
+}
+
+- (NSString *)sanitizedURLStringFromString:(NSString *)string {
+  return [string stringByReplacingOccurrencesOfString:@" " withString:@""];
+}
+
 @end
 
 #endif  // TARGET_OS_IOS || TARGET_OS_TV
diff --git a/FirebaseInAppMessaging/Tests/Unit/FIRIAMFetchResponseParserTests.m b/FirebaseInAppMessaging/Tests/Unit/FIRIAMFetchResponseParserTests.m
index d2acf3f1945..261304e0044 100644
--- a/FirebaseInAppMessaging/Tests/Unit/FIRIAMFetchResponseParserTests.m
+++ b/FirebaseInAppMessaging/Tests/Unit/FIRIAMFetchResponseParserTests.m
@@ -72,7 +72,7 @@ - (void)testRegularConversion {
   XCTAssertEqualWithAccuracy([fetchWaitTime doubleValue],
                              nextFetchEpochTimeInResponse / 1000 - currentMoment, 0.1);
 
-  XCTAssertEqual(6, [results count]);
+  XCTAssertEqual(7, [results count]);
   XCTAssertEqual(0, discardCount);
 
   FIRIAMMessageDefinition *first = results[0];
@@ -147,6 +147,14 @@ - (void)testRegularConversion {
   XCTAssertNotNil(sixth.experimentPayload);
   XCTAssertEqual(FIRIAMRenderAsModalView, sixth.renderData.renderingEffectSettings.viewMode);
   XCTAssertEqual(1, sixth.renderTriggers.count);
+
+  FIRIAMMessageDefinition *seventh = results[6];
+  XCTAssertEqualObjects(@"https://example.com/recoverable_image_url",
+                        seventh.renderData.contentData.imageURL.absoluteString);
+  XCTAssertEqualObjects(nil, seventh.renderData.contentData.landscapeImageURL.absoluteString);
+  XCTAssertEqualObjects(@"http://example.com/recoverable_action_url_without_https",
+                        seventh.renderData.contentData.actionURL.absoluteString);
+  XCTAssertEqualObjects(nil, seventh.renderData.contentData.secondaryActionURL.absoluteString);
 }
 
 - (void)testParsingTestMessage {
diff --git a/FirebaseInAppMessaging/Tests/Unit/TestJsonDataFromFetch.txt b/FirebaseInAppMessaging/Tests/Unit/TestJsonDataFromFetch.txt
index 44f2cd9f193..cadb75300ff 100644
--- a/FirebaseInAppMessaging/Tests/Unit/TestJsonDataFromFetch.txt
+++ b/FirebaseInAppMessaging/Tests/Unit/TestJsonDataFromFetch.txt
@@ -258,6 +258,53 @@
           "fiamTrigger": "ON_FOREGROUND"
         }
       ]
+    },
+    {
+      "vanillaPayload": {
+        "campaignId": "1234567890",
+        "campaignStartTimeMillis": "1519934650000",
+        "campaignEndTimeMillis": "9223372036854775807",
+        "campaignName": "URL Validation"
+      },
+      "content": {
+        "card": {
+          "title": {
+            "text": "Let’s check some weird URLs!",
+            "hexColor": "#004953"
+          },
+          "portraitImageUrl": " https:// example.com/ recoverable_image_url ",
+          "landscapeImageUrl": "http://example.com/image_url_without_https.jpg",
+          "primaryActionButton": {
+            "text": {
+              "text": "Malformatted but Recoverable URL",
+              "hexColor": "#000000"
+            },
+            "buttonHexColor": "#ffffff"
+          },
+          "secondaryActionButton": {
+            "text": {
+              "text": "Invalid URL",
+              "hexColor": "#000000"
+            },
+            "buttonHexColor": "#ffffff"
+          },
+          "primaryAction": {
+            "actionUrl": "http:// example.com / recoverable_action_url_without_https "
+          },
+          "secondaryAction": {
+            "actionUrl": "NOT ^ A ^ URL"
+          },
+          "backgroundHexColor": "#ffffff"
+        }
+      },
+      "priority": {
+        "value": 1
+      },
+      "triggeringConditions": [
+        {
+          "fiamTrigger": "ON_FOREGROUND"
+        }
+      ]
     }
   ],
   "expirationEpochTimestampMillis": "1537896430193"


Test patch: