Skip to content

Commit

Permalink
MIME Checking (#366)
Browse files Browse the repository at this point in the history
* MIME Checking

• Adds the ability for the SDK to use attachments with no file extension
• Also adds the ability to specify the file type using the URL query parameter "file_type"
• If a media attachment URL does not contain a file extension, the SDK will now download the file and check the MIME type of the request to make sure it is a valid file type.

* Change Extension Priorities

• Changes the file extension extraction priorities to be: 1. extract extension from URL itself   2. extract extension from MIME type.  3. extract extension from URL query parameter called 'filename' if provided
• Adds tests to ensure the media URL's are being parsed correctly, and tests the priority of file extension parsing

* Streamline Tests

• Streamlines some code that shouldn't be duplicated

* Cleanup

• Removes URL parsing code that is no longer needed because we've changed the way URL's get parsed
  • Loading branch information
Nightsd01 authored Apr 25, 2018
1 parent a169edc commit a36f78b
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 23 deletions.
12 changes: 12 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@
CA08FC801FE99B25004C445F /* Requests.m in Sources */ = {isa = PBXBuildFile; fileRef = CA08FC7D1FE99B25004C445F /* Requests.m */; };
CA08FC811FE99B25004C445F /* Requests.m in Sources */ = {isa = PBXBuildFile; fileRef = CA08FC7D1FE99B25004C445F /* Requests.m */; };
CA08FC871FE99BB4004C445F /* OneSignalClientOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = CA08FC831FE99BB4004C445F /* OneSignalClientOverrider.m */; };
CA36A42C208FDEFB003EFA9A /* NSURL+OneSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = CA36A42A208FDEFB003EFA9A /* NSURL+OneSignal.h */; };
CA36A42D208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */; };
CA36A42E208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */; };
CA36A42F208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */; };
CA63AF8420211F7400E340FB /* EmailTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CA63AF8320211F7400E340FB /* EmailTests.m */; };
CA63AF8720211FF800E340FB /* UnitTestCommonMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CA63AF8620211FF800E340FB /* UnitTestCommonMethods.m */; };
CA63AFC22022670A00E340FB /* ReattemptRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CA63AFC02022670A00E340FB /* ReattemptRequest.h */; };
Expand Down Expand Up @@ -280,6 +284,8 @@
CA08FC7D1FE99B25004C445F /* Requests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Requests.m; sourceTree = "<group>"; };
CA08FC821FE99BB4004C445F /* OneSignalClientOverrider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalClientOverrider.h; sourceTree = "<group>"; };
CA08FC831FE99BB4004C445F /* OneSignalClientOverrider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalClientOverrider.m; sourceTree = "<group>"; };
CA36A42A208FDEFB003EFA9A /* NSURL+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURL+OneSignal.h"; sourceTree = "<group>"; };
CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURL+OneSignal.m"; sourceTree = "<group>"; };
CA63AF8320211F7400E340FB /* EmailTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EmailTests.m; sourceTree = "<group>"; };
CA63AF8520211FF800E340FB /* UnitTestCommonMethods.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnitTestCommonMethods.h; sourceTree = "<group>"; };
CA63AF8620211FF800E340FB /* UnitTestCommonMethods.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnitTestCommonMethods.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -505,6 +511,8 @@
912412091E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m */,
9124120A1E73342200E41FD7 /* UNUserNotificationCenter+OneSignal.h */,
9124120B1E73342200E41FD7 /* UNUserNotificationCenter+OneSignal.m */,
CA36A42A208FDEFB003EFA9A /* NSURL+OneSignal.h */,
CA36A42B208FDEFB003EFA9A /* NSURL+OneSignal.m */,
);
name = Categories;
sourceTree = "<group>";
Expand Down Expand Up @@ -567,6 +575,7 @@
912412291E73342200E41FD7 /* OneSignalReachability.h in Headers */,
912412251E73342200E41FD7 /* OneSignalMobileProvision.h in Headers */,
91F58D7A1E7C7D3F0017D24D /* OneSignalNotificationSettings.h in Headers */,
CA36A42C208FDEFB003EFA9A /* NSURL+OneSignal.h in Headers */,
CAEA1C66202BB3C600FBFE9E /* OSEmailSubscription.h in Headers */,
91F58D811E7C80C30017D24D /* OneSignalNotificationSettingsIOS8.h in Headers */,
CA08FC7E1FE99B25004C445F /* Requests.h in Headers */,
Expand Down Expand Up @@ -738,6 +747,7 @@
91F58D7D1E7C7F330017D24D /* OneSignalNotificationSettingsIOS10.m in Sources */,
9129C6B81E89E59B009CB6A0 /* OSPermission.m in Sources */,
912412121E73342200E41FD7 /* OneSignalAlertViewDelegate.m in Sources */,
CA36A42D208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */,
912412421E73342200E41FD7 /* UNUserNotificationCenter+OneSignal.m in Sources */,
CA97E14F2051C0A5003B8CB8 /* OneSignalWebOpenDialog.m in Sources */,
9124123A1E73342200E41FD7 /* OneSignalWebView.m in Sources */,
Expand Down Expand Up @@ -777,6 +787,7 @@
0338566D1FBBDB190002F7C1 /* OneSignalTrackFirebaseAnalytics.m in Sources */,
91F58D841E7C88220017D24D /* OneSignalNotificationSettingsIOS10.m in Sources */,
9129C6B91E89E59B009CB6A0 /* OSPermission.m in Sources */,
CA36A42E208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */,
912412131E73342200E41FD7 /* OneSignalAlertViewDelegate.m in Sources */,
CA97E1502051C0A5003B8CB8 /* OneSignalWebOpenDialog.m in Sources */,
0338566B1FBBD2270002F7C1 /* OSNotificationPayload.m in Sources */,
Expand Down Expand Up @@ -834,6 +845,7 @@
4529DED51FA823B900CEAB1D /* TestHelperFunctions.m in Sources */,
911E2CBD1E398AB3003112A4 /* UnitTests.m in Sources */,
CA63AF8420211F7400E340FB /* EmailTests.m in Sources */,
CA36A42F208FDEFB003EFA9A /* NSURL+OneSignal.m in Sources */,
91B6EA431E85D38F00B5CF01 /* OSObservable.m in Sources */,
4529DEDE1FA828E500CEAB1D /* NSDateOverrider.m in Sources */,
CA08FC871FE99BB4004C445F /* OneSignalClientOverrider.m in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions iOS_SDK/OneSignalSDK/Source/NSString+OneSignal.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
- (NSString*)one_getVersionForRange:(NSRange)range;
- (NSString*)one_substringAfter:(NSString *)needle;
- (NSString*)one_getSemanticVersion;
- (NSString *)fileExtensionForMimeType;
- (NSString *)supportedFileExtension;

@end
#endif
15 changes: 15 additions & 0 deletions iOS_SDK/OneSignalSDK/Source/NSString+OneSignal.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//

#import "NSString+OneSignal.h"
#import "OneSignalCommonDefines.h"

#define MIME_MAP @{@"audio/aiff" : @"aiff", @"audio/x-wav" : @"wav", @"audio/mpeg" : @"mp3", @"video/mp4" : @"mp4", @"image/jpeg" : @"jpeg", @"image/jpg" : @"jpg", @"image/png" : @"png", @"image/gif" : @"gif", @"video/mpeg" : @"mpeg", @"video/mpg" : @"mpg", @"video/avi" : @"avi", @"sound/m4a" : @"m4a", @"video/m4v" : @"m4v"}

@implementation NSString (OneSignal)

Expand Down Expand Up @@ -43,5 +46,17 @@ - (NSString*)one_getSemanticVersion {
return (NSString*)tmpstr;
}

- (NSString *)supportedFileExtension {
NSArray <NSString *> *components = [self componentsSeparatedByString:@"."];

if (components.count >= 2 && [ONESIGNAL_SUPPORTED_ATTACHMENT_TYPES containsObject:components.lastObject])
return components.lastObject;

return nil;
}

- (NSString *)fileExtensionForMimeType {
return MIME_MAP[self];
}

@end
34 changes: 34 additions & 0 deletions iOS_SDK/OneSignalSDK/Source/NSURL+OneSignal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Modified MIT License
*
* Copyright 2017 OneSignal
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* 1. The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 2. All copies of substantial portions of the Software may only be used in connection
* with services provided by OneSignal.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#import <Foundation/Foundation.h>

@interface NSURL (OneSignal)

- (NSString *)valueForQueryParameter:(NSString *)parameter;

@end
40 changes: 40 additions & 0 deletions iOS_SDK/OneSignalSDK/Source/NSURL+OneSignal.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Modified MIT License
*
* Copyright 2017 OneSignal
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* 1. The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 2. All copies of substantial portions of the Software may only be used in connection
* with services provided by OneSignal.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#import "NSURL+OneSignal.h"

@implementation NSURL (OneSignal)
- (NSString *)valueForQueryParameter:(NSString *)parameter {
NSURLComponents *components = [NSURLComponents componentsWithURL:self resolvingAgainstBaseURL:false];

for(NSURLQueryItem *item in components.queryItems)
if([item.name isEqualToString:parameter])
return item.value;

return nil;
}
@end
2 changes: 2 additions & 0 deletions iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@
#define ONESIGNAL_FB_LAST_GAF_CAMPAIGN_RECEIVED @"OS_LAST_RECIEVED_GAF_CAMPAIGN"
#define ONESIGNAL_FB_LAST_NOTIFICATION_ID_RECEIVED @"OS_LAST_RECIEVED_NOTIFICATION_ID"

#define ONESIGNAL_SUPPORTED_ATTACHMENT_TYPES @[@"aiff", @"wav", @"mp3", @"mp4", @"jpg", @"jpeg", @"png", @"gif", @"mpeg", @"mpg", @"avi", @"m4a", @"m4v"]

#endif /* OneSignalCommonDefines_h */
78 changes: 57 additions & 21 deletions iOS_SDK/OneSignalSDK/Source/OneSignalHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
#import "OneSignalWebOpenDialog.h"
#import "OneSignalInternal.h"

#import "NSString+OneSignal.h"
#import "NSURL+OneSignal.h"

#import "OneSignalCommonDefines.h"

#define NOTIFICATION_TYPE_ALL 7
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
Expand Down Expand Up @@ -103,12 +108,12 @@ - (id)initWithFilePath:(NSString*)path {
@end

@interface NSURLSession (DirectDownload)
+ (BOOL)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError *)error;
+ (NSString *)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError *)error;
@end

@implementation NSURLSession (DirectDownload)

+ (BOOL)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError *)error {
+ (NSString *)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError *)error {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];

DirectDownloadDelegate *delegate = [[DirectDownloadDelegate alloc] initWithFilePath:localPath];
Expand All @@ -130,10 +135,10 @@ + (BOOL)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSErr
if (error != nil) {
error = downloadError;
}
return false;
return nil;
}

return true;
return delegate.response.MIMEType;
}

@end
Expand Down Expand Up @@ -716,39 +721,70 @@ + (void)addNotificationRequest:(OSNotificationPayload*)payload

}

// Synchroneously downloads a media
// On success returns bundle resource name, otherwise returns nil
/*
Synchroneously downloads an attachment
On success returns bundle resource name, otherwise returns nil
The preference order for file type determination is as follows:
1. File extension in the actual URL
2. MIME type
3. URL Query parameter called 'filename', such as test.jpg. The SDK will extract the file extension from it
*/
+ (NSString*)downloadMediaAndSaveInBundle:(NSString*)urlString {

let inputUrl = [NSURL URLWithString:urlString];
let url = [NSURL URLWithString:urlString];

//removes any unnecessary query parameters that would break extension type checking
let url = [[NSURL alloc] initWithScheme:inputUrl.scheme host:inputUrl.host path:inputUrl.path].absoluteString;
NSString* extension = url.pathExtension;

NSArray<NSString*>* supportedExtensions = @[@"aiff", @"wav", @"mp3", @"mp4", @"jpg", @"jpeg", @"png", @"gif", @"mpeg", @"mpg", @"avi", @"m4a", @"m4v"];
NSArray* components = [url componentsSeparatedByString:@"."];

// URL is not to a file
if ([components count] < 2)
return NULL;
NSString* extension = [components lastObject];
if ([extension isEqualToString:@""])
extension = nil;

// Unrecognized extention
if (![supportedExtensions containsObject:extension])
return NULL;
if (extension != nil && ![ONESIGNAL_SUPPORTED_ATTACHMENT_TYPES containsObject:extension])
return nil;

NSURL* URL = [NSURL URLWithString:url];
var name = [self randomStringWithLength:10];

if (extension)
name = [name stringByAppendingString:[NSString stringWithFormat:@".%@", extension]];

NSString* name = [[self randomStringWithLength:10] stringByAppendingString:[NSString stringWithFormat:@".%@", extension]];
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString* filePath = [paths[0] stringByAppendingPathComponent:name];

//guard against situations where for example, available storage is too low

@try {
NSError* error = nil;
[NSURLSession downloadItemAtURL:URL toFile:filePath error:error];
let mimeType = [NSURLSession downloadItemAtURL:url toFile:filePath error:error];

if (error) {
[OneSignal onesignal_Log:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"Encountered an error while attempting to download file with URL: %@", error]];
return nil;
}

if (!extension) {
NSString *newExtension;

if (mimeType != nil && ![mimeType isEqualToString:@""]) {
newExtension = mimeType.fileExtensionForMimeType;
} else {
newExtension = [[[NSURL URLWithString:urlString] valueForQueryParameter:@"filename"] supportedFileExtension];
}

if (!newExtension || ![ONESIGNAL_SUPPORTED_ATTACHMENT_TYPES containsObject:newExtension])
return nil;

name = [NSString stringWithFormat:@"%@.%@", name, newExtension];

let newPath = [paths[0] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", name]];

[[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newPath error:&error];
}

if (error) {
[OneSignal onesignal_Log:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"Encountered an error while attempting to download file with URL: %@", error]];
return nil;
}

NSArray* cachedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"CACHED_MEDIA"];
NSMutableArray* appendedCache;
if (cachedFiles) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@ + (void)load {
}

// Override downloading of media attachment
+ (BOOL)overrideDownloadItemAtURL:(NSURL*)url toFile:(NSString*)localPath error:(NSError*)error {
+ (NSString *)overrideDownloadItemAtURL:(NSURL*)url toFile:(NSString*)localPath error:(NSError*)error {
NSString *content = @"File Contents";
NSData *fileContents = [content dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:localPath
contents:fileContents
attributes:nil];

return true;
if ([url.absoluteString isEqualToString:@"http://domain.com/file"])
return @"image/png";
else if ([url.absoluteString isEqualToString:@"http://domain.com/secondFile"])
return nil;
else
return @"image/jpg";
}

@end
51 changes: 51 additions & 0 deletions iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1956,4 +1956,55 @@ - (void)testSwizzling {

}

- (UNNotificationAttachment *)deliverNotificationWithJSON:(id)json {
id notifResponse = [UnitTestCommonMethods createBasiciOSNotificationResponseWithPayload:json];

[[notifResponse notification].request.content setValue:@"some_category" forKey:@"categoryIdentifier"];

UNMutableNotificationContent* content = [OneSignal didReceiveNotificationExtensionRequest:[notifResponse notification].request withMutableNotificationContent:nil];

return content.attachments.firstObject;
}

- (id)exampleNotificationJSONWithMediaURL:(NSString *)urlString {
return @{@"aps": @{
@"mutable-content": @1,
@"alert": @"Message Body"
},
@"os_data": @{
@"i": @"b2f7f966-d8cc-11e4-bed1-df8f05be55ba",
@"buttons": @[@{@"i": @"id1", @"n": @"text1"}],
@"att": @{ @"id": urlString }
}};
}

- (void)testExtractFileExtensionFromMimeType {
//test to make sure the MIME type parsing works correctly
//NSURLSessionOverrider returns image/png for this URL
id pngFormat = [self exampleNotificationJSONWithMediaURL:@"http://domain.com/file"];

let downloadedPngFilename = [self deliverNotificationWithJSON:pngFormat].URL.lastPathComponent;
XCTAssertTrue([downloadedPngFilename.supportedFileExtension isEqualToString:@"png"]);
}

- (void)testExtractFileExtensionFromQueryParameter {
// we allow developers to add ?filename=test.jpg (for example) to attachment URL's in cases where there is no extension & no mime type
// tests to make sure the SDK correctly extracts the file extension from the `filename` URL query parameter
// NSURLSessionOverrider returns nil for this URL
id jpgFormat = [self exampleNotificationJSONWithMediaURL:@"http://domain.com/secondFile?filename=test.jpg"];

let downloadedJpgFilename = [self deliverNotificationWithJSON:jpgFormat].URL.lastPathComponent;
XCTAssertTrue([downloadedJpgFilename.supportedFileExtension isEqualToString:@"jpg"]);
}

- (void)testFileExtensionPrioritizesURLFileExtension {
//tests to make sure that the URL's file extension is prioritized above the MIME type and URL query param
//this attachment URL will have a file extension, a MIME type, and a filename query parameter. It should prioritize the URL file extension (gif)
//NSURLSessionOverrider returns image/png for this URL
id gifFormat = [self exampleNotificationJSONWithMediaURL:@"http://domain.com/file.gif?filename=test.png"];

let downloadedGifFilename = [self deliverNotificationWithJSON:gifFormat].URL.lastPathComponent;
XCTAssertTrue([downloadedGifFilename.supportedFileExtension isEqualToString:@"gif"]);
}

@end

0 comments on commit a36f78b

Please sign in to comment.