OSX Automator: Cannot localize an Obj-C action - macos

I have an OSX Automator workflow that contains a custom Obj-C action. It works, but I am not able to localize it. My test action is:
- (id)runWithInput:(id)input fromAction:(AMAction *)anAction error:(NSDictionary **)errorInfo
{
NSArray *contactStrings = (NSArray *)input;
NSString *name = contactStrings[0];
NSString *address = contactStrings[1];
NSString *comment = NSLocalizedString(#"IMPORTED", nil);
NSString *output = [NSString stringWithFormat:#"Name: %#, Address: %#, Comment: %#", name, address, comment];
return output;
}
I have a localized Localizable.strings file with a base and a German localization.
Base (without comments):
"IMPORTED" = "Imported from OSX service";
German (without comments):
"IMPORTED" = "Über OSX-Dienst eingelesen";
The problem is that when the Automator script is executed, the comment string is output as IMPORTED, e.g. the localization does not work at all.
What might be the reason?

I solved the problem:
NSLocalizedString(...) is a macro defined in NSBundle.h as
#define NSLocalizedString(key, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:#"" table:nil]
This means that by using this macro, the Localizable.strings files are searched in the main bundle of the Automator application, not the bundle of the Obj-c action!!! This is also documented here.
The bundle of the Obj-c action is obtained by [self bundle].
I thus replaced the wrong line
NSString *comment = NSLocalizedString(#"IMPORTED", nil);
by
NSString *comment = [[self bundle] localizedStringForKey:#"IMPORTED" value:#"" table:nil];
and everything works.
Hope this helps somebody else also!

Related

Failed attempt using Related Items to create backup file in sandboxed app

The App Sandbox design guide says:
The related items feature of App Sandbox lets your app access files
that have the same name as a user-chosen file, but a different
extension. This feature consists of two parts: a list of related
extensions in the application’s Info.plist file and code to tell the
sandbox what you’re doing.
My Info.plist defines a document type for .pnd files (the user-chosen file), as well as a document type for .bak files. The entry for the .bak files has, among other properties, the property NSIsRelatedItemType = YES.
I am trying to use Related Items to move an existing file to a backup file (change .pnd suffix to .bak suffix) when the user writes a new version of the .pnd file. The application is sandboxed. I am not proficient with sandboxing.
I am using PasteurOrgManager as the NSFilePresenter class for both the original and backup files:
#interface PasteurOrgData : NSObject <NSFilePresenter>
. . . .
#property (readonly, copy) NSURL *primaryPresentedItemURL;
#property (readonly, copy) NSURL *presentedItemURL;
#property (readwrite) NSOperationQueue *presentedItemOperationQueue;
#property (readwrite) NSFileCoordinator *fileCoordinator;
. . . .
- (void) doBackupOf: (NSString*) path;
. . . .
#end
The doBackupOf: method is as follows. Notice that it also sets the NSFilePresenter properties:
- (void) doBackupOf: (NSString*) path
{
NSError *error = nil;
NSString *appSuffix = #".pnd";
NSURL *const pathAsURL = [NSURL URLWithString: [NSString stringWithFormat: #"file://%#", path]];
NSString *const baseName = [pathAsURL lastPathComponent];
NSString *const prefixToBasename = [path substringToIndex: [path length] - [baseName length] - 1];
NSString *const baseNameWithoutExtension = [baseName substringToIndex: [baseName length] - [appSuffix length]];
NSString *backupPath = [NSString stringWithFormat: #"%#/%#.bak", prefixToBasename, baseNameWithoutExtension];
NSURL *const backupURL = [NSURL URLWithString: [NSString stringWithFormat: #"file://%#", backupPath]];
// Move backup to trash — I am sure this will be my next challenge
// (it's a no-op now because there is no pre-existing .bak file)
[[NSFileManager defaultManager] trashItemAtURL: backupURL
resultingItemURL: nil
error: &error];
// Move file to backup
primaryPresentedItemURL = pathAsURL;
presentedItemURL = backupURL;
presentedItemOperationQueue = [NSOperationQueue mainQueue];
[NSFileCoordinator addFilePresenter: self];
fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter: self]; // error here
[self backupItemWithCoordinationFrom: pathAsURL
to: backupURL];
[NSFileCoordinator removeFilePresenter: self];
fileCoordinator = nil;
}
The backupItemWithCoordinationFrom: method does the heavy lifting, basically:
[fileCoordinator coordinateWritingItemAtURL: from
options: NSFileCoordinatorWritingForMoving
error: &error
byAccessor: ^(NSURL *oldURL) {
[self.fileCoordinator itemAtURL: oldURL willMoveToURL: to];
[[NSFileManager defaultManager] moveItemAtURL: oldURL
toURL: to
error: &error];
[self.fileCoordinator itemAtURL: oldURL didMoveToURL: to];
}
but the code doesn't make it that far. I have traced the code and the URL variables are as I expect, and are reasonable. At the point of "error here" in the above code, where I allocate the File Presenter, I get:
NSFileSandboxingRequestRelatedItemExtension: an error was received from pboxd instead of a token. Domain: NSPOSIXErrorDomain, code: 1
[presenter] +[NSFileCoordinator addFilePresenter:] could not get a sandbox extension. primaryPresentedItemURL: file:///Users/cope/Me.pnd, presentedItemURL: file:///Users/cope/Me.bak
Any help is appreciated.
(I have read related posts Where can a sandboxed Mac app save files? and Why do NSFilePresenter protocol methods never get called?. I have taken note of several other sandboxing-related posts that don't seem relevant to this issue.)
MacBook Pro, MacOS 10.13.5, XCode Version 9.3 (9E145)
do not read too much about avoiding sandboxing. Most explenations go too far out of the most obvious problem. Instead of explaining the pitfalls that rightfully triggers sandboxing they explain mostly how to avoid the Sandbox at all. Which is not a solution - it is a thread!
So the most obvious problem is exposing a URL to pasteboard that still needs properly escaped characters in the string before you transform to NSURL.
So your NSString beginning with "file://" should use something like..
NSString *encodeStringForURL = [yourstring stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
before you transform to NSURL with
NSURL *fileurl = [NSURL URLWithString:encodeStringForURL];
NString *output = fileurl.absoluteString;

How do I read from /Library/Application Support/ folder?

I am making a OX Cocoa app and I want to be able to read and write text files using the app on button press. These text files should be saved in /Library/Application Support/AppName but i can't get my app to reading anything from there. It can write to the folder, but not read what it has written there, even though I can see the file sitting there in finder.
Here is the code I am using the successfully write to the folder.
NSString *text = editor.string;
NSString *path = #"/Library/Application Support/";
NSMutableString *mu = [[NSMutableString stringWithString:path] init];
[mu insertString:FileName.stringValue atIndex:mu.length];
[mu insertString:#".txt" atIndex:mu.length];
path = [mu copy];
[text writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:NULL];
Here is the code I am using (and failing) to read from the text files.
NSArray *path = [[NSBundle mainBundle] pathsForResourcesOfType:#"txt" inDirectory:#"/Library/Application Support/"];
NSString *output = #"";
NSMutableString *mu = [[NSMutableString stringWithString:output] init];
for (int i = 0; i < [path count]; i++) {
NSString *text = [NSString stringWithContentsOfFile:path[i] encoding:NSUTF8StringEncoding error:NULL];
[mu insertString:text atIndex:mu.length];
[mu insertString:#"\n" atIndex:mu.length];
}
[textView setString:mu];
Any tips on what I can correct would be super helpful, I'm a bit stuck here.
Edit: Using your input I have updated my code to this:
NSString *fileLocation = #"~/Library/Application Support/";
NSArray *text = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:fileLocation error:nil];
NSString *output = #"";
NSMutableString *mu = [[NSMutableString stringWithString:output] init];
for (int i = 0; i < [text count]; i++) {
[mu insertString:text[i] atIndex:mu.length];
[mu insertString:#"\n" atIndex:mu.length];
}
[textView setString:mu];
However the text from the files is still not appearing.
Most hard-coded paths will fail when you sandbox your app. Even if you get away with this one, or you don't plan to sandbox this app, it's a bad habit that's worth getting out of.
Moreover, are you sure you want /Library and not ~/Library? The former is often not writable by the user. The latter is in the user's Home directory (or your container when sandboxed).
To get the Application Support directory, or the Caches directory, or any other directory that you may want to create things in and later retrieve them from, ask a file manager for it.
/Library/Application Support is not in your bundle. The paths you get using [[NSBundle mainBundle] pathsForResourcesOfType:…] are only useful for accessing files inside your application itself (images, sounds, etc that you included when you built the app).
You want to use [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:error] to get a list of files in a directory outside your application.
Matt Gallagher has a great example of a fault-tolerant method of locating the path to your application support directory at Cocoa With Love. I would recommend using it over hardcoding the /Library/Application Support path.
NSError *error = nil;
NSArray *text = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:fileLocation error:&error];
if (!text) {
NSLog( #"Error reading contents of application support folder at %#.\n%#", applicationSupportFolder, [error userInfo] );
}
You're trying to get the path using NSBundle from the main bundle of the app. But the file is not in the bundle, you should specify the path manually. You could hardcode the path, store previously written paths somewhere, or use NSFileManager to get directory contents and analyze it. For example, -[NSFileManager contentsOfDirectoryAtPath:error:].

How to Search for the unix executable file in OS X programmatically?

So, I need to search for the Unix Executable files in a directory. I Iterate through directory and with the path of the file I am searching. Some of the Methods I tried.
1.With the Help of the file Extension
Unix Executable file does not have the file Extension, but Some documents files are also not having the extensions. So, it failed for me.
2. With the help of NSFileManager
NSDicitionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
It does not have any unique attributes to find the Unix executable file.
3. With the help of MDItemRef
It have the attribute called kMDItemContentType but it is giving the correct result for some of the unix executable files only.
MDItemRef inspectedRef;
CFArrayRef inspectedRefAttributeNames;
CFDictionaryRef inspectedRefAttributeValues;
inspectedRef = MDItemCreate(kCFAllocatorDefault,(CFStringRef)filePath);
if(inspectedRef) {
inspectedRefAttributeNames = MDItemCopyAttributeNames(inspectedRef);
inspectedRefAttributeValues = MDItemCopyAttributes(inspectedRef,inspectedRefAttributeNames);
NSDictionary *attribDict = (__bridge NSDictionary*)inspectedRefAttributeValues;
if([[attribDict objectForKey:#"kMDItemContentType"] isEqualToString:#"public.unix-executable"])
NSLog(#"Unix Executable file");
}
4. With the help of unix command "file"
NSTask *unixTask = [[NSTask alloc] init];
[unixTask setStandardOutput:newPipe];
[unixTask setLaunchPath:#"/usr/bin/file"];
[unixTask setArguments:[NSArray arrayWithObject:filePath]];
[unixTask launch];
[unixTask waitUntilExit];
[unixTask terminationStatus];
while ((inData = [readHandle availableData]) && [inData length]) {
returnValue= [[NSString alloc] initWithData:inData encoding:[NSString defaultCStringEncoding]];
returnValue = [returnValue substringToIndex:[returnValue length]-1];
NSLog(#"%#",returnValue);
}
Here, From the returnValue I can able to find whether it is unix executable or not. But it is very slow Process. So, My question is How to search for the unix executable in an efficient manner ?
Try using either getResourceValue:forKey:error: or resourceValuesForKeys:error: methods of NSURL and requesting NSURLTypeIdentifierKey.
Addendum:
If what #Aravindhanarvi says is correct, on 10.6 there are bugs and the above solution is unreliable. To make things worse #petur solution is also not possible for lack of NSURLIsExecutableKey.
An alternative would be to fall back to the NSFileManager and use methods like isExecutableFileAtPath: and attributesOfItemAtPath:error: (specifically the NSFilePosixPermissions and NSFileType attributes) to implement the same logic suggested by #petur.
Came up with this, just point the url to the directory you to use as the base.
This is ARC code.
The array, files, contains an url pointer to each executable file found.
#autoreleasepool {
NSFileManager *defaultFileManager = [NSFileManager defaultManager];
NSURL *url = [NSURL URLWithString:#"/private/tmp/"]; // Search path
NSDirectoryEnumerator *dirEnumerator = [defaultFileManager enumeratorAtURL:url includingPropertiesForKeys:[NSArray arrayWithObjects:NSURLNameKey, nil] options:0 errorHandler:nil];
NSMutableArray *files = [NSMutableArray array];
// extract non-executable files
for (NSURL *file in dirEnumerator) {
NSNumber *isExecutable;
NSNumber *isDirectory; // Directories have the executable flag set, but we are not interested in them
NSError *error, *error2;
[file getResourceValue:&isExecutable forKey:NSURLIsExecutableKey error:&error];
[file getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error2];
// Deal with errors
if (error)
NSLog(#"%#", [error localizedDescription]);
else if (error2)
NSLog(#"%#", [error2 localizedDescription]);
else if ([isExecutable boolValue] && ![isDirectory boolValue]) {
[files addObject:file];
}
// print out all executable files to the console
for (id i in files)
NSLog(#"%#", [i description]);
}

Reading a plist

I'm trying to read ~/Library/Preferences/com.apple.mail.plist (on Snow Leopard) to get the email address and other information to enter into the about dialog. I'm using the following code, which is obviously wrong:
NSBundle* bundle;
bundle = [NSBundle mainBundle];
NSString *plistPath = [bundle pathForResource:#"~/Library/Preferences/com.apple.mail.plist" ofType:#"plist"];
NSDictionary *plistData = [NSDictionary dictionaryWithContentsOfFile:plistPath];
NSString *item = [plistData valueForKeyPath:#"MailAccounts.Item 2.AccountName"];
NSLog(#"Result = %#", item);
Moreover, the value I need to read is MailAcounts -> Item 2 -> AccountName and I am not sure I am doing this correctly (due to the space in the Item 2 key).
I tried reading Apple's developer guide to plist files but no help there.
How can I read a plist and extract the values as an NSString?
Thanks.
The first level is an array, so you need to use "MailAccounts.AccountName" and treat it as NSArray*:
NSString *plistPath = [#"~/Library/Preferences/com.apple.mail.plist" stringByExpandingTildeInPath];
NSDictionary *plistData = [NSDictionary dictionaryWithContentsOfFile:plistPath];
NSArray *item = [plistData valueForKeyPath:#"MailAccounts.AccountName"];
NSLog(#"Account: %#", [item objectAtIndex:2]);
Alternatively you can go by keys and pull the array from "MailAccounts" first using valueForKey: (which will yield NSArray*) and then objectAtIndex: to get the dictionary of that particular account (useful if you need more than the name).
Two things:
You don't want or need to use NSBundle to get the path to the file. The file lies outside of the app bundle. So you should just have
NSString *plistPath = #"~/Library/Preferences/com.apple.mail.plist";
You have to expand the tilde in the path to the user directory. NSString has a method for this. Use something like
NSString *plistPath = [#"~/Library/Preferences/com.apple.mail.plist" stringByExpandingTildeInPath];

How to use NSTask as root?

In an application I'm making I need to run the following command as root (user will be prompted trice if they really want to, and they will be asked to unmount their drives) using NSTask:
/bin/rm -rf /
#Yes, really
The problem is that simply using Substitute User Do (sudo) doesn't work as the user needs to enter the password to the non-available stdin. I'd rather like to show the user the same window as you'd see when you click the lock in Preferences.app, like this (hopefully with a shorter password):
(source: quickpwn.com)
Can anyone help me with this? Thanks.
Check out STPrivilegedTask, an Objective-C wrapper class around AuthorizationExecuteWithPrivileges() with an NSTask-like interface.
That's one of the hardest tasks to do properly on Mac OS X.
The guide documenting how to do this is the Authorization Services Programming Guide. There are multiple possibilities, as usual the most secure is the hardest to implement.
I've started writing a tool that uses a launchd daemon (the most secure way), the code is available on google code. So if you want, you can copy that code.
I think I can now answer this, thanks to some Googling and a nice find in this SO question. It's very slightly hacky, but IMHO is a satisfactory solution.
I wrote this generic implementation which should achieve what you want:
- (BOOL) runProcessAsAdministrator:(NSString*)scriptPath
withArguments:(NSArray *)arguments
output:(NSString **)output
errorDescription:(NSString **)errorDescription {
NSString * allArgs = [arguments componentsJoinedByString:#" "];
NSString * fullScript = [NSString stringWithFormat:#"%# %#", scriptPath, allArgs];
NSDictionary *errorInfo = [NSDictionary new];
NSString *script = [NSString stringWithFormat:#"do shell script \"%#\" with administrator privileges", fullScript];
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&errorInfo];
// Check errorInfo
if (! eventResult)
{
// Describe common errors
*errorDescription = nil;
if ([errorInfo valueForKey:NSAppleScriptErrorNumber])
{
NSNumber * errorNumber = (NSNumber *)[errorInfo valueForKey:NSAppleScriptErrorNumber];
if ([errorNumber intValue] == -128)
*errorDescription = #"The administrator password is required to do this.";
}
// Set error message from provided message
if (*errorDescription == nil)
{
if ([errorInfo valueForKey:NSAppleScriptErrorMessage])
*errorDescription = (NSString *)[errorInfo valueForKey:NSAppleScriptErrorMessage];
}
return NO;
}
else
{
// Set output to the AppleScript's output
*output = [eventResult stringValue];
return YES;
}
}
Usage example:
NSString * output = nil;
NSString * processErrorDescription = nil;
BOOL success = [self runProcessAsAdministrator:#"/usr/bin/id"
withArguments:[NSArray arrayWithObjects:#"-un", nil]
output:&output
errorDescription:&processErrorDescription
asAdministrator:YES];
if (!success) // Process failed to run
{
// ...look at errorDescription
}
else
{
// ...process output
}
Okay, so I ran into this while searching how to do this properly... I know it's probably the least secure method of accomplishing the task, but probably the easiest and I haven't seen this answer anywhere. I came up with this for apps that I create to run for my own purposes and as a temporary authorization routine for the type of task that user142019 is describing. I don't think Apple would approve. This is just a snippet and does not include a UI input form or any way to capture stdout, but there are plenty of other resources that can provide those pieces.
Create a blank file called "script.sh" and add it to your project's supporting files.
Add this to header file:
// set this from IBOutlets or encrypted file
#property (strong, nonatomic) NSString * password;
#property (strong, nonatomic) NSString * command;
implementation:
#synthesize password;
#synthesize command;
(IBAction)buttonExecute:(id)sender {
NSString *scriptPath = [[NSBundle mainBundle]pathForResource:#"script" ofType:#"sh"];
NSString *scriptText = [[NSString alloc]initWithFormat:#"#! usr/sh/echo\n%# | sudo -S %#",password,command];
[scriptText writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSTask * task = [[NSTask alloc]init];
[task setLaunchPath:#"/bin/sh"];
NSArray * args = [NSArray arrayWithObjects:scriptPath, nil];
[task setArguments:args];
[task launch];
NSString * blank = #" ";
[blank writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
The last step is just so you don't have a cleartext admin password sitting in your bundle. I recommend making sure that anything beyond the method be obfuscated in some way.
In one of my cases this is not correct:
>
The problem is that simply using Substitute User Do (sudo) doesn't work as the user needs to enter the password
>
I simply edited /etc/sudoers to allow the desired user to start any .sh script without prompting for password. So you would execute a shell script which contains a command line like sudo sed [...] /etc/printers.conf to modify the printers.conf file, and the /etc/sudoers file would contain this line
myLocalUser ALL=(ALL) NOPASSWD: ALL
But of course I am looking for a better solution which correctly prompts the user to type in an admin password to allow the script or NSTask to execute. Thanks for the code which uses an AppleScript call to prompt and execute the task/shell script.

Resources