How to use iOS security scoped bookmarks across devices? - ios

On iOS (14/15) I'm trying to pass security scoped bookmarks to user picked files on iCloud Drive between devices but whatever I try: I cannot get urls to be restored on another device running the same app.
The app is a UIDocument based app, the code below is in a UIViewController that display the document. The document is created like so:
Document* document = [[Document alloc] initWithFileURL:documentURL];
and then passed on to the ViewController
The url that's going to be bookmarked is picked using a plain UIDocumentPickerViewController
This is how I create a security scoped bookmark:
// Toggle this to create either a document scoped url or an app scoped url
static BOOL DOC_SCOPE = NO;
- (void) documentPicker:(UIDocumentPickerViewController*)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls {
if (urls.count > 0) {
NSURL* url = urls.firstObject;
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
BOOL doClose = [url startAccessingSecurityScopedResource];
NSData* bookmark = [url bookmarkDataWithOptions:0 includingResourceValuesForKeys:nil relativeToURL:DOC_SCOPE?docURL:nil error:nil];
if (doClose)
[url stopAccessingSecurityScopedResource];
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
NSString* encoded = [NSString stringWithFormat:#"\n[%#]\n", [bookmark base64EncodedStringWithOptions:0]];
At this point I insert the encoded bookmark data in the document data and save the document.
When opening the linked document the bookmark and url are restored like so:
openLink:(NSString*)encoded {
NSData* bookmark = [[NSData alloc] initWithBase64EncodedString:encoded options:0];
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
NSError* error = nil;
NSURL* url = [NSURL URLByResolvingBookmarkData:bookmark options:0 relativeToURL:DOC_SCOPE?docURL:nil bookmarkDataIsStale:nil error:&error];
if (error != nil)
NSLog(#"%#", error.localizedFailureReason);
if (url != nil) {
BOOL doClose = [url startAccessingSecurityScopedResource];
// here use the url to access linked file
if (doClose)
[url stopAccessingSecurityScopedResource];
}
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
}
To the project's entitlements I have added
com.apple.security.files.bookmarks.app-scope = 1
com.apple.security.files.bookmarks.document-scope = 1
When on the same device I can the restore the bookmark data and get access to the restored URL OK, but when opening the same file on another device, [NSURL URLByResolvingBookmarkData...] always sets an error:
Error Domain=NSCocoaErrorDomain Code=257 "The file couldn’t be opened because you don’t have permission to view it."
This is both the case for app scope book marks and document scope book marks.
Any idea what's missing / how to get this working?

Related

iOS Creating a file in a public folder

I want to let the user choose the folder, where I'd save a new file.
To achieve that, I use document picker, and set the document type to public.folder and inMode UIDocumentPickerModeOpen.
After user opens the document picker and selects the desired folder, in didPickDocumentsAtURLs callback I get the NSUrl object, which has permissions to modify the file at that url (in this case, it's an url to a folder).
There is my issue. I have the url with access permission to a folder, however, to create a file I ussualy need to have the filename.extension in the url. If I were to modify the NSUrl object I've received from the document picker, or convert it to NSString, my guess is I lose the access permission and createFileAtPath method always fails.
What method do I need to use, or what configuration document picker do I need, in order to create a new file in the path that the user selected? I attach my current code:
- (void)openDocumentPicker:(NSString*)pickerType
{
//Find the current app window, and its view controller object
UIApplication* app = [UIApplication sharedApplication];
UIWindow* rootWindow = app.windows[0];
UIViewController* rootViewController = rootWindow.rootViewController;
//Initialize the document picker
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:#[pickerType] inMode:UIDocumentPickerModeOpen];
//Assigning the delegate, connects the document picker object with callbacks, defined in this object
documentPicker.delegate = self;
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
//Call the document picker, to the view controller that we've found before
[rootViewController presentViewController:documentPicker animated:YES completion:nil];
}
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
//If we come here, user successfully picked a file/folder
[urls[0] startAccessingSecurityScopedResource]; //Let the os know we're going to use the file
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentsDirectory = urls[0].absoluteString;
NSString *newFilePath = [documentsDirectory stringByAppendingPathComponent:#"test.txt"];
NSError *error = nil;
if ([fileManager createFileAtPath:newFilePath contents:[#"new file test" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]){
NSLog(#"Create Sucess");
}
else{
NSLog(#"Create error: %#", error);
}
[urls[0] stopAccessingSecurityScopedResource]; //Let the os know we're done
}
Any leads would be kindly appreciated!
This is solution in swift, please try and let me knwo if any problem
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]){
var imgData: Data?
if let url = urls.first{
imgData = try? Data(contentsOf: url)
do{
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let destURLPath = documentDirectory.appendingPathComponent(url.lastPathComponent)
try imgData?.write(to: URL(fileURLWithPath: destURLPath))
print("FILES IS Writtern at DOcument Directory")
}catch{
}
}
}
To answer my own question, I'll leave a fully working code bellow.
My main issue was, that when you're using "public.folder" document type, you need to call startAccessingSecurityScopedResource with the url of the chosen folder, not with the modified link (file the user chose + NewFileName.extension)
- (void)openDocumentPicker
{
//This is needed, when using this code on QT!
//Find the current app window, and its view controller object
/*
UIApplication* app = [UIApplication sharedApplication];
UIWindow* rootWindow = app.windows[0];
UIViewController* rootViewController = rootWindow.rootViewController;
*/
//Initialize the document picker. Set appropriate document types
//When reading: use document type of the file, that you're going to read
//When writing into a new file: use #"public.folder" to select a folder, where your new file will be created
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:#[#"public.folder"] inMode:UIDocumentPickerModeOpen];
//Assigning the delegate, connects the document picker object with callbacks, defined in this object
documentPicker.delegate = self;
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
//In this case we're using self. If using on QT, use the rootViewController we've found before
[self presentViewController:documentPicker animated:YES completion:nil];
}
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
//If we come here, user successfully picked a single file/folder
//When selecting a folder, we need to start accessing the folder itself, instead of the specific file we're going to create
if ( [urls[0] startAccessingSecurityScopedResource] ) //Let the os know we're going use this resource
{
//Write file case ---
//Construct the url, that we're going to be using: folder the user chose + add the new FileName.extension
NSURL *destURLPath = [urls[0] URLByAppendingPathComponent:#"Test.txt"];
NSString *dataToWrite = #"This text is going into the file!";
NSError *error = nil;
//Write the data, thus creating a new file. Save the new path if operation succeeds
if( ![dataToWrite writeToURL:destURLPath atomically:true encoding:NSUTF8StringEncoding error:&error] )
NSLog(#"%#",[error localizedDescription]);
//Read file case ---
NSData *fileData = [NSData dataWithContentsOfURL:destURLPath options:NSDataReadingUncached error:&error];
if( fileData == nil )
NSLog(#"%#",[error localizedDescription]);
[urls[0] stopAccessingSecurityScopedResource];
}
else
{
NSLog(#"startAccessingSecurityScopedResource failed");
}
}
This was also being discussed at apple forums:
Thread name: "iOS Creating a file in a public folder"
Thread link:
https://developer.apple.com/forums/thread/685170?answerId=682427022#682427022

UIDocumentPickerViewController with UIDocumentPickerModeOpen mode not giving permission to edit the file in xcode (iOS)

I want the user to select a file from files app and I've to read the contents of that file, modify it and write it in the same location.
I was trying to open the file using the following code:
UIDocumentPickerViewController *documentProvider;
documentProvider = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:[NSArray arrayWithObjects:#"public.comma-separated-values-text", nil] inMode: UIDocumentPickerModeOpen];
documentProvider.delegate = self;
documentProvider.modalPresentationStyle = UIModalPresentationOverFullScreen;
Delegate function:
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
{
NSLog(#"%#", url.absoluteString);
}
But when I try to view or edit the file, it's showing error that I don't have view/write permission.
The URL that I received is file:///private/var/mobile/Library/Mobile%20Documents/com~apple~CloudDocs/filename.csv
Is there any way to get permission for the files in iCloud? I found few third party apps which can modify the contents of the file.
You need to access the file via a security scoped bookmark. Like such:
- (void) documentPicker: (UIDocumentPickerViewController *) controller
didPickDocumentAtURL: (NSURL *) url
{
if (controller.documentPickerMode == UIDocumentPickerModeOpen)
{
BOOL isAccess = [url startAccessingSecurityScopedResource];
if(!isAccess)
{
return;
}
NSError * fileOpenError = nil;
NSString * fileContents = [NSString stringWithContentsOfURL: url
encoding: NSUTF8StringEncoding
error: &fileOpenError];
// FileContents should now be set properly.
[url stopAccessingSecurityScopedResource];
}
}

Not able to fetch images in extension app from App group shared container in iOS 10

In my host App I am downloading custom emojis images folder after unzipping successfully saving by below url.
NSURL* shareContainerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.company.app.PushServiceExtn"];
And without any issue whenever user tapping on emojis icon all the custom emojis shows in grid in place of keyboard by shareContainerURL.
I have created PushNotification Service Extension where I need to show the custom emojis image by fetching emoji name from payload whenever push comes. using below code.
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSDictionary* mediaAttachment = [self.bestAttemptContent.userInfo objectForKey:#"media-attachment"];
NSString* attachType = [mediaAttachment objectForKey:#"attachType"];
if ([attachType isEqualToString:#"emoji"]) {
NSString* strEmojiURL = [mediaAttachment objectForKey:#"url"];
self.bestAttemptContent.title = strEmojiURL;
NSString* emojiName = [[strEmojiURL stringByRemovingPercentEncoding] lastPathComponent];
NSString* strUnpresseedEmojiPath = [self getFullPath:#"emoji/Pressed"];
NSString* strImagePath = [NSString stringWithFormat:#"%#/%# Pressed.png",strUnpresseedEmojiPath, emojiName];
NSURL* fileURL = [NSURL fileURLWithPath:strImagePath];
NSData *imageData = [NSData dataWithContentsOfURL:fileURL];
UIImage *image = [UIImage imageWithData:imageData];
if (image) {
NSError* error;
// CGRect rect = CGRectMake(0,0,50,50);
// #{UNNotificationAttachmentOptionsThumbnailClippingRectKey:(__bridge NSDictionary*)CGRectCreateDictionaryRepresentation(rect)} option dict;
UNNotificationAttachment * attachement = [UNNotificationAttachment attachmentWithIdentifier:strImagePath.lastPathComponent URL:fileURL options:nil error:&error];
if (error == nil) {
self.bestAttemptContent.attachments = #[attachement];
}
}
}
self.contentHandler(self.bestAttemptContent);
}
- (NSString *)getFullPath:(NSString *)file {
NSURL* shareContainerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.company.app.PushServiceExtn"];
return [shareContainerURL.path stringByAppendingPathComponent: file];
}
I am always getting valid url but second time I get image nil but first time of every image it works. Couldn't get the root cause. Any help would appreciated.
Below is the error that occurred second time for every image.
2016-10-27 17:34:59.081026 pushNotificationServiceExtension[651:34632] Attachement Error = Error Domain=UNErrorDomain Code=100 "Invalid attachment file URL" UserInfo={NSLocalizedDescription=Invalid attachment file URL}
Also please let me know how to view App Group shared container, Couldn't find way to view the files contained inside.
*Update = * File is getting deleted after showing in push notification.
From apple "UNNotificationAttachment Once validated, attached files are moved into the attachment data store so that they can be accessed by the appropriate processes. Attachments located inside an app’s bundle are copied instead of moved."
So I copy my emoji image to duplicate URL and assign it to UNNotificationAttachment.
if (imageFileURL) {
NSURL* duplicateImageURL = [self getFullPath:#"EmojiAttachment"];
if (![fileManager fileExistsAtPath:duplicateImageURL.path]) {
[fileManager createDirectoryAtPath:duplicateImageURL.path withIntermediateDirectories:NO attributes:nil error:&error];
}
emojiName = [NSString stringWithFormat:#"%# Unpressed.png", emojiName];
duplicateImageURL = [duplicateImageURL URLByAppendingPathComponent:emojiName];
[[NSFileManager defaultManager]copyItemAtURL:imageFileURL toURL:duplicateImageURL error:&error];
UNNotificationAttachment * attachement = [UNNotificationAttachment attachmentWithIdentifier:emojiName URL:[duplicateImageURL filePathURL] options:nil error:&error];
if (error == nil) {
self.bestAttemptContent.attachments = #[attachement];
}
else{
NSLog(#"Attachement Error = %#",error);
}
}

iOS how to insert a local file URL into NSURLConnection response data?

I'm trying to modify a behavior of a webpage within my iOS app and make the in-page media player play a file from the local caches folder instead of fetching it from a web server.
Below is my code that replaces the http:// video path with a local file path. The code does not work, giving me "Resource Temporary not available. Please try again" error message popup. Is it possible to have a web-based media player play file from a local disk using file URL?
I tried substituting these for the instanceURL, but they don't seem to work.
[fileURL path]
[fileURL absolutePath]
I'm intercepting the request for the file and am parsing it to find out that the page is asking for a video file:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
// An NSURLConnection delegate callback. We pass this on to the client.
{
NSDictionary* decisionDictionary = [[RequestListener sharedInstance] shouldContinue:connection processRequestData:data];
BOOL shouldContinue = [decisionDictionary[#"shouldContinue"] boolValue];
if(shouldContinue == NO)
{
return;
}else
{
NSData* d = data;
//substitute fake data
if(decisionDictionary[#"data"])
{
d = decisionDictionary[#"data"];
}
[[self client] URLProtocol:self didLoadData:d];
}
}
Within my shouldContinue method, I check if the video is present locally and modify the response data to create a path to a local video.
NSString* path = [VideoDownloader localVideoPathForVideoID:videoID];
NSURL* fileURL = [NSURL fileURLWithPath:path];
DLog(#"url:%#",[fileURL absoluteString]);
NSString* replacement = [NSString stringWithFormat:#"\"instanceUrl\":\"%#\"",[[fileURL absoluteURL] absoluteString]];
DLog(#"replacement:%#",replacement);
NSString* forgedResponse = [parts componentsJoinedByString:#","];
NSData* forgedData = [forgedResponse dataUsingEncoding:NSUTF8StringEncoding];
return #{#"shouldContinue":#(YES),#"data":forgedData};
Have a look at NSURLProtocol. You can intercept http requests before they are sent to a host to decide what to do about it: Continue to server, redirect to local cache.
There's a decent tutorial by our beloved Ray Wenderlich.
Apple has a programming guide as well.

How to check if a file exists in Documents folder?

I have an application with In-App Purchase, that when the user buy something, download one html file into the Documents folder of my app.
Now I must check if this HTML file exists, so if true, load this HTML file, else load my default html page.
How I can do that? With NSFileManager I can't get outside of mainBundle..
Swift 3:
let documentsURL = try! FileManager().url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
... gives you a file URL of the documents directory. The following checks if there's a file named foo.html:
let fooURL = documentsURL.appendingPathComponent("foo.html")
let fileExists = FileManager().fileExists(atPath: fooURL.path)
Objective-C:
NSString* documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString* foofile = [documentsPath stringByAppendingPathComponent:#"foo.html"];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:foofile];
Apple recommends against relying on the fileExistAtPath: method. It's often better to just try to open a file and deal with the error if the file does not exist.
NSFileManager Class Reference
Note: Attempting to predicate behavior based on the current state of the file system or a particular file on the file system is not recommended. Doing so can cause odd behavior or race conditions. It's far better to attempt an operation (such as loading a file or creating a directory), check for errors, and handle those errors gracefully than it is to try to figure out ahead of time whether the operation will succeed. For more information on file system race conditions, see “Race Conditions and Secure File Operations” in Secure Coding Guide.
Source: Apple Developer API Reference
From the secure coding guide.
To prevent this, programs often check to make sure a temporary file with a specific name does not already exist in the target directory. If such a file exists, the application deletes it or chooses a new name for the temporary file to avoid conflict. If the file does not exist, the application opens the file for writing, because the system routine that opens a file for writing automatically creates a new file if none exists.
An attacker, by continuously running a program that creates a new temporary file with the appropriate name, can (with a little persistence and some luck) create the file in the gap between when the application checked to make sure the temporary file didn’t exist and when it opens it for writing. The application then opens the attacker’s file and writes to it (remember, the system routine opens an existing file if there is one, and creates a new file only if there is no existing file).
The attacker’s file might have different access permissions than the application’s temporary file, so the attacker can then read the contents. Alternatively, the attacker might have the file already open. The attacker could replace the file with a hard link or symbolic link to some other file (either one owned by the attacker or an existing system file). For example, the attacker could replace the file with a symbolic link to the system password file, so that after the attack, the system passwords have been corrupted to the point that no one, including the system administrator, can log in.
If you set up your file system differently or looking for a different way of setting up a file system and then checking if a file exists in the documents folder heres an another example. also show dynamic checking
for (int i = 0; i < numberHere; ++i){
NSFileManager* fileMgr = [NSFileManager defaultManager];
NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents"];
NSString* imageName = [NSString stringWithFormat:#"image-%#.png", i];
NSString* currentFile = [documentsDirectory stringByAppendingPathComponent:imageName];
BOOL fileExists = [fileMgr fileExistsAtPath:currentFile];
if (fileExists == NO){
cout << "DOESNT Exist!" << endl;
} else {
cout << "DOES Exist!" << endl;
}
}
Swift 2.0
This is how to check if the file exists using Swift
func isFileExistsInDirectory() -> Bool {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDirectory: AnyObject = paths[0]
let dataPath = documentsDirectory.stringByAppendingPathComponent("/YourFileName")
return NSFileManager.defaultManager().fileExistsAtPath(dataPath)
}
check if file exist in side the document/catchimage path :
NSString *stringPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
NSString *tempName = [NSString stringWithFormat:#"%#/catchimage/%#.png",stringPath,#"file name"];
NSLog(#"%#",temName);
if([[NSFileManager defaultManager] fileExistsAtPath:temName]){
// ur code here
} else {
// ur code here**
}
NSArray *directoryPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *imagePath = [directoryPath objectAtIndex:0];
//If you have superate folder
imagePath= [imagePath stringByAppendingPathComponent:#"ImagesFolder"];//Get docs dir path with folder name
_imageName = [_imageName stringByAppendingString:#".jpg"];//Assign image name
imagePath= [imagePath stringByAppendingPathComponent:_imageName];
NSLog(#"%#", imagePath);
//Method 1:
BOOL file = [[NSFileManager defaultManager] fileExistsAtPath: imagePath];
if (file == NO){
NSLog("File not exist");
} else {
NSLog("File exist");
}
//Method 2:
NSData *data = [NSData dataWithContentsOfFile:imagePath];
UIImage *image = [UIImage imageWithData:data];
if (!(image == nil)) {//Check image exist or not
cell.photoImageView.image = image;//Display image
}
NSURL.h provided - (BOOL)checkResourceIsReachableAndReturnError:(NSError **)error to do so
NSURL *fileURL = [NSURL fileURLWithPath:NSHomeDirectory()];
NSError * __autoreleasing error = nil;
if ([fileURL checkResourceIsReachableAndReturnError:&error]) {
NSLog(#"%# exists", fileURL);
} else {
NSLog(#"%# existence checking error: %#", fileURL, error);
}
Or using Swift
if let url = URL(fileURLWithPath: NSHomeDirectory()) {
do {
let result = try url.checkResourceIsReachable()
} catch {
print(error)
}
}

Resources