NSOpenPanel under Sandbox - Access All Files inside User Selected folder - macos

My sandboxed macOS app imports image files selected by the user via an NSOpenPanel modal window, as is customary.
At first, I configured the panel to canChooseDirectories = false, and set the allowedFileTypes property to NSImage.imageTypes. So far so good.
Using the app, I realized that the images I want to import are more often than not all grouped inside a folder with nothing more in it. It would be great if I could have the user just select the containing folder and import the images within "wholesale", so I adopted this code:
let panel = NSOpenPanel()
panel.allowsMultipleSelection = true
panel.canChooseDirectories = true
panel.canCreateDirectories = false
panel.canChooseFiles = true
panel.allowedFileTypes = NSImage.imageTypes
panel.begin { [unowned self] (result) in
guard result == .OK else {
return // User cancelled
}
// Read all selected images:
let urls: [URL] = {
if let directory = panel.directoryURL {
// .........................................
// [A] One directory selected:
do {
let urls = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])
return urls
} catch {
// (I ALWAYS END UP HERE)
print(error.localizedDescription)
return []
}
} else {
// .........................................
// [B] One or more files selected:
return panel.urls
}
}()
// (next: read individual urls...)
...but the try statement always fails, the catch block is executed and the error thrown is:
"The file “MyImageFolder” couldn’t be opened because you don’t have permission to view it."
Is there a way around this for sandboxed apps? Anything that I am forgetting, that will allow me to read the contents of a user-selected folder?
Addendum: Apple's documentation states that:
When a user of your app specifies they want to use a file or a folder, the system adds the associated path to your app’s sandbox. Say, for example, a user drags the ~/Documents folder onto your app’s Dock tile (or onto your app’s Finder icon, or into an open window of your app), thereby indicating they want to use that folder. In response, the system makes the ~/Documents folder, its contents, and its subfolders available to your app.
(emphasis mine)

I accepted #vadian's quick answer a bit prematurely, but it seems that I can access the individual files inside the user-selected folder from NSOpenPanel.
After reading this answer (that I somehow missed at first in my searches), I found out that the code below works:
// Warning! This code does not deal with the user selecting
// multiple folders!
let urls: [URL] = {
if inputURLs.count == 1, inputURLs[0].hasDirectoryPath {
// Folder; Read its contents:
do {
let urls = try FileManager.default.contentsOfDirectory(at: inputURLs[0], includingPropertiesForKeys: nil, options: [])
return urls
} catch {
// (todo: Handle Errors)
return []
}
} else {
// One or more images; Read them directly:
return inputURLs
}
}()
An additional mistake I seemed to be making is to use NSURL's isFileURL property to distinguish between a folder selected and a single file: it returns true for folder too!
So after I switched from using panel.directoryURL to using panel.urls[0] (when isFileURL is true), my app was trying to read a single image from the directory URL. No sandbox violation, but no image read either.
According to the docs, that property returns true "if the receiver uses the file scheme" (whatever that means). I guess folders too "use the file scheme".
I switched to using hasDirectoryPath instead, as suggested in this other answer.

Related

Howto add a custom link provider

In the latest release of vscode (1__49), there is a code snippet on creating a new link provider. https://code.visualstudio.com/updates/v1_49. I can't seem to find a reference on where to apply this code.
window.registerTerminalLinkProvider({
provideTerminalLinks: (context, token) => {
// Detect the first instance of the word "test" if it exists and linkify it
const startIndex = (context.line as string).indexOf('test');
if (startIndex === -1) {
return [];
}
// Return an array of link results, this example only returns a single link
return [
{
startIndex,
length: 'test'.length,
tooltip: 'Show a notification',
// You can return data in this object to access inside handleTerminalLink
data: 'Example data'
}
];
},
handleTerminalLink: (link: any) => {
vscode.window.showInformationMessage(`Link activated (data = ${link.data})`);
}
});
What is the process for getting the editor to utilize this feature?
You will need to create a vscode extension that includes your code.
As it so happens, I have just set up a fresh extension that will use the TerminalLinkProvider. You can take a look at how the sample code integrates into a sample extension on GitHub.
A good place to start with your first extension is the official guide.
After that, just add your code to the activate(...) function of your extension.
You can built your extension as a .vsix file and install it in any vscode instance you use, but if you think that your code might be of value to others, consider publishing it!

How to associate existing file type for Mac OS application in XCode 7?

I am making a simple viewer for jpeg pictures in Xcode 7, and trying to associate Jpeg file type with my application. This is a Cocoa application for OS X in Swift that uses storyboards, and is not a document-based application.
Tutorials that I found online suggest going through Info tab, adding a new document type there. Now that's where things start to look different from those in tutorials: in them there is only 3 field to fill out (Name, Types, Icon), but I have many more. I tried to experiment around and here is what I put in fields that I got:
Name: JPEG image
Class: left it blank
Extensions: .jpg, .jpeg, .JPEG, .JPG
Icon: left it blank
Identifier: public.jpeg
Role: Viewer
Mime types: image/jpeg
"Document is distributed as a bundle" is partially checked by default; I just left it as is.
Did not touch additional document type properties either.
As a result I got my application showing in a list in "Open with" when I secondary-click a Jpeg file along with other installed applications, but once I try to open it that way, I get a popup saying The document "picture.jpg" could not be opened. MyApp cannot open files in the "JPEG image" format.
What am I doing wrong?
The Class field is compulsory, and you have to have corresponding implementation.
Try:
Class: $(PRODUCT_MODULE_NAME).Document
And then add the Document.swift:
import Cocoa
class Document : NSDocument
{
override init() {
super.init()
// Add your subclass-specific initialization here.
}
override class func autosavesInPlace() -> Bool {
return true
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
// Insert code here to write your document to data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override fileWrapperOfType:error:, writeToURL:ofType:error:, or writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
// Insert code here to read your document from the given data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning false.
// You can also choose to override readFromFileWrapper:ofType:error: or readFromURL:ofType:error: instead.
// If you override either of these, you should also override -isEntireFileLoaded to return false if the contents are lazily loaded.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
^ Modify as you need.
Also, you specified the role "Viewer", means you can open it with the space bar, and not the double click (open) - which is the role "Edit" right?

How to move a file and create missing directories in OS X?

I want to move a file, in OSX, to another directory:
func moveFile(currentPath currentPath: String, targetPath: String) {
let fileManager = NSFileManager.defaultManager()
do { try fileManager.moveItemAtPath(currentPath, toPath: targetPath) }
catch let error as NSError { print(error.description) }
}
Everything is working fine, except the case when the target-directory doesn't exist. I figured out that .isWritableFileAtPath could be helpful.
However, in my declared function I use the full file path (including the filename).
How can I split the filename from the path or more in general: how can I force Swift to create the directory before moving the file if needed?
In the past I have solved this problem with code similar to the code below. Basically you just check to see if a file exists at the path representing the parent directory of the file you want to create. If it does not exist you create it and all folders above it in the path that don't exist as well.
func moveFile(currentPath currentPath: String, targetPath: String) {
let fileManager = NSFileManager.defaultManager()
let parentPath = (targetPath as NSString).stringByDeletingLastPathComponent()
var isDirectory: ObjCBool = false
if !fileManager.fileExistsAtPath(parentPath, isDirectory:&isDirectory) {
fileManager.createDirectoryAtPath(parentPath, withIntermediateDirectories: true, attributes: nil)
// Check to see if file exists, move file, error handling
}
else if isDirectory {
// Check to see if parent path is writable, move file, error handling
}
else {
// Parent path exists and is a file, error handling
}
}
You may also want to use the fileExistsAtPath:isDirectory: variant so you can handle other error cases. Same is true for
I've added this extension to FileManager to achieve this
extension FileManager {
func moveItemCreatingIntermediaryDirectories(at: URL, to: URL) throws {
let parentPath = to.deletingLastPathComponent()
if !fileExists(atPath: parentPath.path) {
try createDirectory(at: parentPath, withIntermediateDirectories: true, attributes: nil)
}
try moveItem(at: at, to: to)
}
}
Adding this because this question pops up in Google and the other answers use an API that they perhaps shouldn't in this context.
It's important to note this in the FileExists(atPath:) docs:
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.
Also, from the createDirectory(atPath:withIntermediateDirectories:attributes:) docs:
Return Value
true if the directory was created, true if createIntermediates is set and the directory already exists, or false if an error occurred.
Attempting to create a new directory with the withIntermediateDirectories: parameter set to true will not throw an error if that directory already exists, so you can safely use it even if the directory does already exist.
Skip the existence check, try to write the directory, then try to move the file:
func moveFile(from currentURL: URL, to targetURL: URL) {
// Get the target directory by removing the file component
let targetDir = targetURL.deletingLastPathComponent()
do {
try FileManager.default.createDirectory(at:targetDir, withIntermediateDirectories: true, attributes: nil)
} catch {
// Handle errors
}
do {
try FileManager.default.moveItem(at:currentURL, to: targetURL) }
catch {
// Handle errors
}
}

How to get a path out of an NSSavePanel object in Swift

I have been attempting to use the NSSavePanel to save text files in a program written in Swift. The only issue is that every time I attempt to use the URL attribute, it has a nil value. Yes, I have a folder selected and a file name in the input when testing. Here's my code:
let saveDialog = NSSavePanel();
saveDialog.beginWithCompletionHandler() { (result: Int) -> Void in
if result == NSFileHandlingPanelOKButton {
let file = NSFileHandle(forWritingToURL: saveDialog.URL!, error: nil)!;
for match in Globals.matches {
if let data = (match.toString() as NSString).dataUsingEncoding(NSUTF8StringEncoding) {
file.writeData(data);
}
}
}
}
// other setup code not shown
When I run this, I always get the Swift equivalent of a null-pointer exception on the
let file = NSFileHandle(forWritingToURL: saveDialog.URL!, error: nil)!;
line. Can I please have some help? What am I doing wrong?
Check the documentation for NSFileHandle(forWritingToURL:error:), it says:
The initialized file handle object or nil if no file exists at url.
So this only works if the file already exists. Which is probably not what you want.
It looks like NSFileHandle cannot create new files at all, so I would just use the following before you try to open the file:
NSFileManager.defaultManager()
.createFileAtPath(saveDialog.URL.path, contents: NSData(), attributes: nil)
Which will create an empty file, which you can then open and write your data to.

Get Firefox to run XUL type script on startup

With Firefox 17.0.1 I am using an add-on called KeyConfig 20110522 to set some new hot keys and also set the acceltext of menuitems for my new keys as well as for add-ons that do not bother to do so.
I want the acceltext of the menuitems to be set when Firefox starts, but currently I am just using a hot key to execute the following code against the UI via KeyConfig:
document.getElementById("tabmix-menu")
.setAttribute("acceltext","Alt+Ctrl+Shift+T");
// more of the same...
I need a couple of beginners tips:
How can I execute arbitrary code against the UI in the same way as I execute against an HTML page via the console?
Is there a sneaky way to get a clump of code to execute on browser start-up without delving into XUL development?
Is there a way to trace commands executed against the UI so I can get at command calls instead of using triggers when I set my hot keys like so:
document.getElementById("tabmix-menu").click();
Any other tips on this type of low-level hacking would also be welcome.
You can execute arbitrary code against the Firefox UI from an addon, but as you say, doing all the XUL related stuff is a bit boring :-)
Enter "Bootstrapped" extensions!
Part 1:
A "Bootstrapped" (or re-startless) extension needs only an install.rdf file to identify the addon, and a bootstrap.js file to implement the bootstrap interface.
Bootstrapped Extension: https://developer.mozilla.org/en-US/docs/Extensions/Bootstrapped_extensions
Good example: http://blog.fpmurphy.com/2011/02/firefox-4-restartless-add-ons.html
The bootstrap interface can be implemented very simply:
function install() {}
function uninstall() {}
function shutdown(data, reason) {}
function startup(data, reason) { /* YOUR ARBITRARY CODE HERE! */ }
You compile the extension by putting install.rdf and bootstrap.js into the top-level of a new zip file, and rename the zip file extension to .xpi.
Part 2:
Your code is privileged and can use any of the Mozilla platform APIs. There is however an issue of timing. The moment-in-time at which the "startup" function is executed is one at which no Chrome window objects exist yet!
If it's important for your code that you have a Chrome Window, we need to wait for it to appear:
// useful services.
Cu.import("resource://gre/modules/Services.jsm");
var loader = Cc["#mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
var wmSvc = Cc["#mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
var logSvc = Cc["#mozilla.org/consoleservice;1"]
.getService(Ci.nsIConsoleService);
// get the first gBrowser
var done_startup = 0;
var windowListener;
function do_startup(win) {
if (done_startup) return;
done_startup = 1;
wmSvc.removeListener(windowListener);
var browserEnum = wmSvc.getEnumerator("navigator:browser");
var browserWin = browserEnum.getNext();
var tabbrowser = browserWin.gBrowser;
/* your code goes here! */
}
// window listener implementation
windowListener = {
onWindowTitleChange: function(aWindow, aTitle) {},
onCloseWindow: function(aWindow) {},
onOpenWindow: function(aWindow) {
var win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
win.addEventListener("load", function(aEvent) {
win.removeEventListener("load", arguments.callee, false);
if (aEvent.originalTarget.nodeName != "#document") return;
do_startup();
}
};
// CODE ENTRY POINT (put this in bootstrap "startup" function)
wmSvc.addListener(windowListener);

Resources