macOS accessibility API on WebKit applications with AXTextMarker - objective-c

I need to access data from webkit applications such as Safari, Mail and maybe others. I can see in the Accessibility Inspector there is :AXTextMarker and AXTextMarkerForRange.
I tried the usual way to get this info :
AXUIElementRef systemWideElement = AXUIElementCreateSystemWide(); //creating system wide element
AXUIElementRef focussedElement = NULL;
AXError error = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute, (CFTypeRef *)&focussedElement); //Copy the focused
if (error != kAXErrorSuccess){
NSLog(#"Could not get focused element");
}else{
AXValueRef marker = NULL;
AXError getTextValueError = AXUIElementCopyAttributeValue(focussedElement, kAXMarkerUIElementsAttribute , (CFTypeRef *)&marker);
}
kAXMarkerUIElementsAttribute is the only thing I can see with Marker but everything is empty each time.
I guess for security reasons, I cannot access them? Is there any way possible. I am developing an app for people with difficulties reading and it could really help.
Thanks

Tips and tricks:
Ask focussedElement for its supported attributes. Use the functions:
AXError AXUIElementCopyAttributeNames(AXUIElementRef element, CFArrayRef _Nullable *names);
Returns a list of all the attributes supported by the specified accessibility object.
and
AXError AXUIElementCopyParameterizedAttributeNames(AXUIElementRef element, CFArrayRef _Nullable *names);
Returns a list of all the parameterized attributes supported by the specified accessibility object.
Most undocumented attributes are self explanatory.
For example get the selected text as attributed string:
CFTypeRef markerRange = NULL;
AXError error = AXUIElementCopyAttributeValue(focussedElement, (CFStringRef)#"AXSelectedTextMarkerRange", &markerRange);
CFTypeRef result = NULL;
error = AXUIElementCopyParameterizedAttributeValue(focussedElement, (CFStringRef)#"AXAttributedStringForTextMarkerRange", markerRange, &result);

Try this, I haven't tested this but I found some research on the API and I think this may solve your problem.
It seems for the accessibility API to fully work the application needs to be trusted or basically have root access. Is the application running in root?
I found some apple script from a past project. seemed relevant
NSDictionary *errorInfo = [NSDictionary new];
NSString *script = [NSString stringWithFormat:#"do shell script \"%# %#\" with administrator privileges", #"/usr/sbin/lsof",#"-c filecoord"];
NSLog(#"Running... %#",script);
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&errorInfo];
This apple script isn't excatily what you need but its a start. I'm not sure how to fully request root access to an application from inside the application its self,
But for testing, you can always just run your application as root and continue your debugging
Please let me know if this helped you in any way or not

Accessibility apps need to be added in System Preferences in the following section:
System Preferences -> Security & Privacy -> Privacy tab -> Accessibility (on the left)
Note that during development you need to include Xcode in this list if you are developing/running within Xcode.
I didn't test out your exact code, but I tested some comparable swift code and that worked fine provided Xcode was added in that spot in system preferences.

Related

Asking a user for elevated privileges and elevating the application without an Apple developer certificate

Apparently, as of 10.7, AuthorizationExecuteWithPrivileges is deprecated. The general gist of the information I've gathered on this seems to suggest using ServiceManagement.framework's SMJobBless() function to have a helper application deployed.
My understanding of it though, is that this will need a developer certificate to be purchased from Apple to code sign both my application and the helper process - or this will not work. Is this correct?
I originally used AuthorizationExecuteWithPrivileges to ask a user for elevated privileges, since they are needed to access another running process. Without that, my app can't work as the unofficial plugin it's intended to. Is the code signing way really the only way to go from here? I'm trying to avoid purchasing a developer certificate due to the sheer cost of it.
Has anyone found any alternative ways to relaunch an application with elevated privileges, with user permission of course?
#CarlosP's answer with code to escape the path & arguments:
- (BOOL)runProcessAsAdministrator:(NSString*)scriptPath
withArguments:(NSArray*)arguments
output:(NSString**)output
errorDescription:(NSString**)errorDescription {
//Check path.
if (![scriptPath hasPrefix:#"/"]) {
#throw [NSException exceptionWithName:
NSInvalidArgumentException reason:#"Absolute path required." userInfo:nil];
}
//Define script.
static NSAppleScript* appleScript = nil;
if (!appleScript) {
appleScript = [[NSAppleScript alloc] initWithSource:
#"on run commandWithArguments\n"
" activate\n"
" repeat with currentArgument in commandWithArguments\n"
" set contents of currentArgument to quoted form of currentArgument\n"
" end repeat\n"
" set AppleScript's text item delimiters to space\n"
" return do shell script (commandWithArguments as text) with administrator privileges\n"
"end run"];
}
//Set command.
NSAppleEventDescriptor* commandWithArguments = [NSAppleEventDescriptor listDescriptor];
[commandWithArguments insertDescriptor:
[NSAppleEventDescriptor descriptorWithString:scriptPath] atIndex:0];
//Set arguments.
for (NSString* currentArgument in arguments) {
[commandWithArguments insertDescriptor:
[NSAppleEventDescriptor descriptorWithString:currentArgument] atIndex:0];
}
//Create target & event.
ProcessSerialNumber processSerial = {0, kCurrentProcess};
NSAppleEventDescriptor* scriptTarget =
[NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&processSerial length:sizeof(ProcessSerialNumber)];
NSAppleEventDescriptor* scriptEvent =
[NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass
eventID:kAEOpenApplication
targetDescriptor:scriptTarget
returnID:kAutoGenerateReturnID
transactionID:kAnyTransactionID];
[scriptEvent setParamDescriptor:commandWithArguments forKeyword:keyDirectObject];
//Run script.
NSDictionary* errorInfo = [NSDictionary dictionary];
NSAppleEventDescriptor* eventResult = [appleScript executeAppleEvent:scriptEvent error:&errorInfo];
//Success?
if (!eventResult) {
if (errorDescription)
*errorDescription = [errorInfo objectForKey:NSAppleScriptErrorMessage];
return NO;
} else {
if (output)
*output = [eventResult stringValue];
return YES;
}
}
Update
In Yosemite, do shell script just calls a version of AuthorizationExecuteWithPrivileges embedded in StandardAdditions.osax.
It's conceivable that the with administrator privileges option for do shell script will go away when AuthorizationExecuteWithPrivileges does.
Personally, I would just continue calling AuthorizationExecuteWithPrivileges directly.
do shell script does have the advantage of reaping the process automatically. That requires a little extra work with AuthorizationExecuteWithPrivileges.
Is the code signing way really the only way to go from here?
To my knowledge, there is no secure alternative to AuthorizationExecuteWithPrivileges.
It still works fine under Yosemite. Haven't tried out El Capitan yet.
You can try to fail gracefully if the call goes away in the future.
I'm trying to avoid purchasing a developer certificate due to the sheer cost of it.
Well, if it helps, the code signing certificate will be valid for several years.
I'm pretty sure I've let my developer account lapse without any issues.
So it's basically $99 every five years.

Text Replace Service with Xcode - Not replacing selected text

I am trying to build a standalone system service (app with .service extension, saved to ~/Library/Services/) to replace user-selected text in Mac OS X.
I want to build it with Xcode and not Automator, because I am more accustomed to Objective-C than Applescript.
I found several examples on the internet, e.g. this and also Apple's documentation. I got the Xcode project appropriately configured and building without problems. However, when I install my service and try to use it, nothing happens.
The service method itself is executed: I placed code to show an NSAlert inside its method body and it shows. However, the selected text does not get replaced.
Any idea what might be missing? This is the method that implements the service:
- (void) fixPath:(NSPasteboard*) pboard
userData:(NSString*) userData
error:(NSString**) error
{
// Make sure the pasteboard contains a string.
if (![pboard canReadObjectForClasses:#[[NSString class]] options:#{}])
{
*error = NSLocalizedString(#"Error: the pasteboard doesn't contain a string.", nil);
return;
}
NSString* pasteboardString = [pboard stringForType:NSPasteboardTypeString];
//NSAlert* alert = [[NSAlert alloc] init];
//[alert setMessageText:#"WORKING!"];
//[alert runModal];
// ^ This alert is displayed when selecting the service in the context menu
pasteboardString = #"NEW TEXT";
NSArray* types = [NSArray arrayWithObject:NSStringPboardType];
[pboard clearContents];
[pboard declareTypes:types owner:nil];
// Set new text:
[pboard writeObjects:[NSArray arrayWithObject:pasteboardString]];
// Alternatively:
[pboard setString:pasteboardString forType:NSStringPboardType];
// (neither works)
return;
}
After careful reading of Apple's documentation, I found the answer: My service app's plist file was missing a key under the Services section:
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
I only had the opposite NSSendTypes key, which lets you send data from the client app to the service. This one is needed to send the modified text back (in the other direction).
It is weird because, Apple's documentation seems to imply that specifying these two is no longer necessary since 10.6 (Snow Leopard).
For (hopefully) useful console spew, in terminal type:
defaults write -g ViewBridgeLogging -bool YES
Note: useful for services and extensions also.

ScriptingBridge without sdef? (cocoa)

I'd like to get properties of the currently active app. I understand that this should be possible with ScriptingBridge, however, this seems to require you generate an sdef file and import this in your project for the app you are trying to target. Since I want to target all apps, is there another way to do this?
Example of accessing system preferences:
SystemPreferencesApplication *systemPreferences =
[SBApplication
applicationWithBundleIdentifier:#"com.apple.systempreferences"];
If there's another way to access properties of any active app, please do share. (For example; window title)
Thanks.
I assume you want to run an applescript. The scripting bridge is good if you have a lot of applescript code to run. However if you only have a small amount then a simpler way is with NSApplescript.
For example if you wanted to run this applescript...
tell application "System Events"
set theProcesses to processes
repeat with aProcess in theProcesses
tell aProcess to get properties
end repeat
end tell
Then you can write it this way...
NSString* cmd = #"tell application \"System Events\"\nset theProcesses to processes\nrepeat with aProcess in theProcesses\ntell aProcess to get properties\nend repeat\nend tell";
NSAppleScript* theScript = [[NSAppleScript alloc] initWithSource:cmd];
NSDictionary* errorDict = nil;
NSAppleEventDescriptor* result = [theScript executeAndReturnError:&errorDict];
[theScript release];
if (errorDict) {
NSLog(#"Error:%# %#", [errorDict valueForKey:#"NSAppleScriptErrorNumber"], [errorDict valueForKey:#"NSAppleScriptErrorMessage"]);
return;
}
// do something with result
NSLog(#"result: %#", result);
You can get a list of every currently running Application with
NSWorkSpace.sharedWorkspace.runningApplications;
Each object in that array should be an NSRunningApplication, which you can query and manipulate freely.

Global events, the Mac App Store, and the sandbox

I'm working on an app where using global key-down events will be a requirement for its operation. Additionally, I plan on distributing this strictly via the App Store. (It's a Mac app, not iOS.) I've gotten an example of listening for the global events working via addGlobalMonitorForEventsMatchingMask, but with caveats.
Note: I am making the choice to use the modern API's and not rely on the earlier Carbon hotkey methods. In the event that they are deprecated eventually, I don't want to have to figure this problem out later.
The principle issue is that the app has to be trusted in order for global events to be detected. Otherwise, accessibility has to be enabled for all apps. When I enable accessibility, events are detected successfully. This requirement is documented here, https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/MonitoringEvents/MonitoringEvents.html.
I would prefer that for my users, they will not have to enable accessibility. From other research I've done, you can get an application to be trusted by calling AXMakeProcessTrusted, then restarting the application.
In the code that I'm using, I do not get an authentication prompt. The app will restart, but is still not trusted (likely because I don't get an authentication prompt). Here's my code for this part:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
if (!AXAPIEnabled() && !AXIsProcessTrusted()) {
NSString *appPath = [[NSBundle mainBundle] bundlePath];
AXError error = AXMakeProcessTrusted( (CFStringRef)CFBridgingRetain(appPath) );
[self restartApp];
}
}
- (void)restartApp{
NSTask *task = [[NSTask alloc] init];
NSMutableArray *args = [NSMutableArray array];
[args addObject:#"-c"];
[args addObject:[NSString stringWithFormat:#"sleep %d; open \"%#\"", 3, [[NSBundle mainBundle] bundlePath]]];
[task setLaunchPath:#"/bin/sh"];
[task setArguments:args];
[task launch];
[NSApp terminate:nil];
}
Further, I've looked at the documentation for Authorization Service Tasks here https://developer.apple.com/library/archive/documentation/Security/Conceptual/authorization_concepts/03authtasks/authtasks.html#//apple_ref/doc/uid/TP30000995-CH206-BCIGAIAG.
The first thing that worries me that pops out is this info box, "Important The authorization services API is not supported within an app sandbox because it allows privilege escalation."
If this API is required to get the authentication prompt before restarting the app, it seems that I may not be able to get global events without the accessibility feature enabled.
In summary, my specific questions are:
Is there an error in my sample code about how to get the
authentication prompt to appear?
In order to get the authentication prompt to appear, am I required
to use the Authorization Services API?
Is it possible, or not possible, to have a sandboxed app that has
access to global events?
First of all, there is no way you can automatically allow an app to use accessibility API which would work in a sandbox environment and thus in app store. The recommended way is to simply guide users so they can easily enable it themselves. The new API call AXIsProcessTrustedWithOptions is exactly for that:
NSDictionary *options = #{(id) kAXTrustedCheckOptionPrompt : #YES};
AXIsProcessTrustedWithOptions((CFDictionaryRef) options);
Now, to your first and second question (just for the sake of completeness - again it won't work in sandbox):
The idea behind AXMakeProcessTrusted was that you actually create a new auxiliary application that you run as root from the main application. This utility then calls AXMakeProcessTrusted passing in the executable of the main application. Finally you have to restart the main app. The API call has been deprecated in OSX 10.9.
To spawn a new process as a root you have to use launchd using SMJobSubmit. This will prompt a user with an authentication prompt saying that an application is trying to install a helper tool and whether it should be allowed. Concretely:
+ (BOOL)makeTrustedWithError:(NSError **)error {
NSString *label = FMTStr(#"%#.%#", kShiftItAppBundleId, #"mktrusted");
NSString *command = [[NSBundle mainBundle] pathForAuxiliaryExecutable:#"mktrusted"];
AuthorizationItem authItem = {kSMRightModifySystemDaemons, 0, NULL, 0};
AuthorizationRights authRights = {1, &authItem};
AuthorizationFlags flags = kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights;
AuthorizationRef auth;
if (AuthorizationCreate(&authRights, kAuthorizationEmptyEnvironment, flags, &auth) == errAuthorizationSuccess) {
// this is actually important - if from any reason the job was not removed, it won't relaunch
// to check for the running jobs use: sudo launchctl list
// the sudo is important since this job runs under root
SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
// this is actually the launchd plist for a new process
// https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html#//apple_ref/doc/man/5/launchd.plist
NSDictionary *plist = #{
#"Label" : label,
#"RunAtLoad" : #YES,
#"ProgramArguments" : #[command],
#"Debug" : #YES
};
BOOL ret;
if (SMJobSubmit(kSMDomainSystemLaunchd, (CFDictionaryRef) plist, auth, (CFErrorRef *) error)) {
FMTLogDebug(#"Executed %#", command);
ret = YES;
} else {
FMTLogError(#"Failed to execute %# as priviledged process: %#", command, *error);
ret = NO;
}
// From whatever reason this did not work very well
// seems like it removed the job before it was executed
// SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef) label, auth, false, NULL);
AuthorizationFree(auth, 0);
return ret;
} else {
FMTLogError(#"Unable to create authorization object");
return NO;
}
}
As for the restarting, this is usually done also using an external utility to which waits for a main application to finish and starts it again (by using PID). If you use sparkle framework you can reuse the existing one:
+ (void) relaunch {
NSString *relaunch = [[NSBundle bundleForClass:[SUUpdater class]] pathForResource:#"relaunch" ofType:#""];
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *pid = FMTStr(#"%d", [[NSProcessInfo processInfo] processIdentifier]);
[NSTask launchedTaskWithLaunchPath:relaunch arguments:#[path, pid]];
[NSApp terminate:self];
}
Another option is to hack the /Library/Application Support/com.apple.TCC/TCC.db sqlite database add the permissions manually using an auxiliary helper:
NSString *sqlite = #"/usr/bin/sqlite3";
NSString *sql = FMTStr(#"INSERT or REPLACE INTO access values ('kTCCServiceAccessibility', '%#', 1, 1, 1, NULL);", MY_BUNDLE_ID);
NSArray *args = #[#"/Library/Application Support/com.apple.TCC/TCC.db", sql];
NSTask *task = [NSTask launchedTaskWithLaunchPath:sqlite arguments:args];
[task waitUntilExit];
This however will disqualify the app from being app store. More over it is really just a hack and the db / schema can change any time. Some applications (e.g. Divvy.app used to do this) used this hack within the application installer post install script. This way prevents the dialog telling that an app is requesting to install an auxiliary tool.
Basically, MAS restrictions will require you to the route of having tge user turning on AX for all.
I found a potential solution on GitHub.
https://github.com/K8TIY/CW-Station
It has an auxiliary application which would be run at root to request access for the main application. It is a little outdated and is using some functions which have been deprecated so I am working on modernizing it. It looks like a good starting point.

Get list of installed apps on iPhone

Is there a way (some API) to get the list of installed apps on an iPhone device.
While searching for similar questions, I found some thing related to url registration, but I think there must be some API to do this, as I don't want to do any thing with the app, I just want the list.
No, apps are sandboxed and Apple-accepted APIs do not include anything that would let you do that.
You can, however, test whether a certain app is installed:
if the app is known to handle URLs of a certain type
by using [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:#"thisapp://foo"]
You can get a list of apps and URL schemes from here.
For jailbroken devices you can use next snipped of code:
-(void)appInstalledList
{
static NSString* const path = #"/private/var/mobile/Library/Caches/com.apple.mobile.installation.plist";
NSDictionary *cacheDict = nil;
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath: path isDirectory: &isDir] && !isDir)
{
cacheDict = [NSDictionary dictionaryWithContentsOfFile: path];
NSDictionary *system = [cacheDict objectForKey: #"System"]; // First check all system (jailbroken) apps
for (NSString *key in system)
{
NSLog(#"%#",key);
}
NSDictionary *user = [cacheDict objectForKey: #"User"]; // Then all the user (App Store /var/mobile/Applications) apps
for (NSString *key in user)
{
NSLog(#"%#",key);
}
return;
}
NSLog(#"can not find installed app plist");
}
for non jailbroken device, we can use third party framework which is called "ihaspp", also its free and apple accepted. Also they given good documentation how to integrate and how to use. May be this would be helpful to you. Good luck!!
https://github.com/danielamitay/iHasApp
You could do this by using the following:
Class LSApplicationWorkspace_class = objc_getClass("LSApplicationWorkspace");
SEL selector = NSSelectorFromString(#"defaultWorkspace");
NSObject* workspace = [LSApplicationWorkspace_class performSelector:selector];
SEL selectorALL = NSSelectorFromString(#"allApplications");
NSMutableArray *Allapps = [workspace performSelector:selectorALL];
NSLog(#"apps: %#", Allapps);
And then by accessing each element and splitting it you can get your app name, and even the Bundle Identifier, too.
Well, not sure if this was available back when the last answer was given or not (Prior to iOS 6)
Also this one is time intensive, yet simple:
Go into settings > Gen. >usage. The first category under usage at least right now is Storage.
It will show a partial list of apps. At the bottom of this partial list is a button that says "show all apps".
Tap that and you'll have to go through screen by screen, and take screenshots (Quick lock button and home button takes a screenshot).
I'm doing this now and I have hundreds of apps on my iPhone. So it's going to take me a while. But at least at the end of the process I'll have Images of all my apps.