Files
Objective-See Foundation c8a0f6498f bug fixes/project cleanup
v2.2.0
friends updates
removed error reporting
removed old code (pre-macOS 11)

...more changes / improvements soon!
2025-04-30 14:16:30 -07:00

2480 lines
65 KiB
Objective-C
Executable File

//
// AppDelegate.m
// TaskExplorer
//
#import <syslog.h>
#import "Task.h"
#import "Consts.h"
#import "Binary.h"
#import "Update.h"
#import "Utilities.h"
#import "Connection.h"
#import "AppDelegate.h"
#import "serviceInterface.h"
#import "TaskTableController.h"
#import "RequestRootWindowController.h"
//TODO: autolayout vertically
@implementation AppDelegate
@synthesize topPane;
@synthesize filterObj;
@synthesize vtThreads;
@synthesize bottomPane;
@synthesize saveButton;
@synthesize currentTask;
@synthesize searchButton;
@synthesize viewSelector;
@synthesize taskViewFormat;
@synthesize commandHandling;
@synthesize completePosting;
@synthesize filteringMessage;
@synthesize filteringOverlay;
@synthesize customItemsFilter;
@synthesize customTasksFilter;
@synthesize progressIndicator;
@synthesize filteringIndicator;
@synthesize taskTableController;
@synthesize bottomViewController;
@synthesize aboutWindowController;
@synthesize searchWindowController;
@synthesize updateWindowController;
@synthesize resultsWindowController;
@synthesize flagItemsWindowController;
@synthesize requestRootWindowController;
//kick off main window/logic
-(void)taskExplore
{
//for autolayout
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints"];
//init virus total object
virusTotal = [[VirusTotal alloc] init];
//init filter obj
filterObj = [[Filter alloc] init];
//alloc/init custom search field for tasks
customTasksFilter = [[CustomTextField alloc] init];
//set owner
self.customTasksFilter.owner = self;
//alloc/init custom search field for items
customItemsFilter = [[CustomTextField alloc] init];
//set owner
self.customItemsFilter.owner = self;
//set field editor for tasks
[self.customTasksFilter setFieldEditor:YES];
//set field editor for items
[self.customItemsFilter setFieldEditor:YES];
//center
[self.window center];
//first responder: filter tasks
[self.window makeFirstResponder:self.filterTasksBox];
//make main window front
[self.window makeKeyAndOrderFront:self];
//make app front
[NSApp activateIgnoringOtherApps:YES];
//check
[self check4Update:nil];
//register for hotkey presses
[self registerKeypressHandler];
//check if authenticated
// display authentication request if needed
if(YES != [self isAuthenticated])
{
//wait to allow app to become front
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, .33 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
//display auth popup
// will invoke 'go' method on successful auth
[self askForRoot];
});
}
//go!
// setup tracking areas and begin thread that explores tasks
else
{
//go!
[self go];
}
//set default top pane view to flat
self.taskViewFormat = FLAT_VIEW;
//set initial view for top pane
// ->default is flat (non-tree) view
[self changeViewController];
//alloc/init bottom pane controller
self.bottomViewController = [[TaskTableController alloc] initWithNibName:@"FlatView" bundle:nil];
//set flag
self.bottomViewController.isBottomPane = YES;
//disable autosize translations
self.bottomViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
//add subview
[self.bottomPane addSubview:self.bottomViewController.view];
//set frame
[[self.bottomViewController view] setFrame:[self.bottomPane bounds]];
//constrain to parent
[self constrainView:self.bottomPane subView:self.bottomViewController.view];
//add 'items' not found msg
[self.bottomViewController.view addSubview:self.noItemsLabel];
//set delegate
// ->ensures our 'windowWillClose' method, which has logic to fully exit app
self.window.delegate = self;
return;
}
//automatically invoked by OS
// ->main entry point
-(void)applicationDidFinishLaunching:(NSNotification *)notification
{
//toggle away
[[[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.loginwindow"] firstObject] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
//toggle back
// work-around for menu not showing since we set Application is agent(UIElement): YES
[[[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.objective-see.TaskExplorer"] firstObject] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
//first time run?
// show thanks to friends window!
// note: on close, invokes method to show main window
if(YES != [[NSUserDefaults standardUserDefaults] boolForKey:NOT_FIRST_TIME])
{
//set key
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:NOT_FIRST_TIME];
//front
[self.friends makeKeyAndOrderFront:self];
//front
[NSApp activateIgnoringOtherApps:YES];
//make first responder
// calling this without a timeout sometimes fails :/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
//and make it first responder
[self.friends makeFirstResponder:[self.friends.contentView viewWithTag:1]];
});
//close after 3 seconds
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
//hide
[self hideFriends:nil];
});
}
//start task exploring
else
{
//show
[self taskExplore];
}
return;
}
//register handler for hot keys
-(void)registerKeypressHandler
{
//handler
NSEvent * (^keypressHandler)(NSEvent *) = nil;
//handler block
keypressHandler = ^NSEvent * (NSEvent * theEvent){
return [self handleKeypress:theEvent];
};
//register for key-down events
[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:keypressHandler];
return;
}
//invoked for any (but only) key-down events
-(NSEvent*)handleKeypress:(NSEvent*)event
{
//flag indicating event was handled
BOOL wasHandled = NO;
//only care about 'cmd' + something
if(NSCommandKeyMask != (event.modifierFlags & NSCommandKeyMask))
{
//bail
goto bail;
}
//handle key-code
// refresh (cmd+r)
// save (cmd+s)
// search (cmd+f)
// close window (cmd+w)
// info for selected task (cmd+i)
switch ([event keyCode])
{
//'r' (refresh)
case KEYCODE_R:
//refresh
[self refreshTasks:nil];
//set flag
wasHandled = YES;
break;
//'f' (find, search)
case KEYCODE_F:
//find
[self search:nil];
//set flag
wasHandled = YES;
break;
//'s' (save)
case KEYCODE_S:
//save
[self saveResults:nil];
//set flag
wasHandled = YES;
break;
//'w' (close window)
case KEYCODE_W:
//close
// ->if not main window
if(self.window != [[NSApplication sharedApplication] keyWindow])
{
//close window
[[[NSApplication sharedApplication] keyWindow] close];
//set flag
wasHandled = YES;
}
break;
default:
break;
}
//bail
bail:
//nil out event if it was handled
if(YES == wasHandled)
{
event = nil;
}
// Return the event, a new event, or, to stop
// the event from being dispatched, nil
return event;
}
//hide friends view
// also shows/kicks off main window
- (IBAction)hideFriends:(id)sender
{
//once
static dispatch_once_t onceToken;
//close and launch main window
dispatch_once (&onceToken, ^{
//close
[self.friends close];
//start task exploring
[self taskExplore];
});
return;
}
//complete a few inits
// ->then invoke helper method to start enum'ing task (in bg thread)
-(void)go
{
//init mouse-over areas
[self initTrackingAreas];
//go!
[self exploreTasks];
//bail
bail:
return;
}
//check if app is auth'd
// ->specifically, if XPC service is setuid
-(BOOL)isAuthenticated
{
//flag
BOOL isAuthenticated = NO;
//path to XPC service
NSString* xpcService = nil;
//file attributes
NSDictionary* fileAttributes = nil;
//get path to XPC service
xpcService = getPath2XPC();
//sanity check
if(nil == xpcService)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to get path to XPC service");
//bail
goto bail;
}
//get XPC services' attributes
fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:xpcService error:nil];
//sanity check
if(nil == fileAttributes)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to file attributes for XPC service");
//bail
goto bail;
}
//check if (fully) auth'd
// ->owned by r00t & SETUID
if( (0 == [fileAttributes[NSFileOwnerAccountID] unsignedLongValue]) &&
(0 != (S_ISUID & [fileAttributes[NSFilePosixPermissions] unsignedLongValue])) )
{
//set flag
isAuthenticated = YES;
}
//bail
bail:
return isAuthenticated;
}
//display window that asks for r00t
// ->if user agrees, execs cmd to set XPC to be setuid!
-(void)askForRoot
{
//alloc/init request window
if(nil == self.requestRootWindowController)
{
//alloc/init
requestRootWindowController = [[RequestRootWindowController alloc] initWithWindowNibName:@"RequestRootWindow"];
}
//center window
[[self.requestRootWindowController window] center];
//show it
[self.requestRootWindowController showWindow:self];
//invoke function in background that will make window modal
// ->waits until window is non-nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//make modal
makeModal(self.requestRootWindowController);
});
return;
}
//begin task enumeration
-(void)exploreTasks
{
//alloc task enumerator
if(nil == taskEnumerator)
{
//alloc
taskEnumerator = [[TaskEnumerator alloc] init];
}
//kick off thread to enum task
// ->will update table as results come in
[NSThread detachNewThreadSelector:@selector(enumerateTasks:) toTarget:taskEnumerator withObject:nil];
return;
}
//change top pane
// ->switch between either flat (default) or tree-based (hierachical) view
-(void)changeViewController
{
//remove old view
if([self.taskTableController view] != nil)
{
//remove
[[self.taskTableController view] removeFromSuperview];
//'free'
self.taskTableController = nil;
}
switch(self.taskViewFormat)
{
case FLAT_VIEW:
{
//alloc/init
taskTableController = [[TaskTableController alloc] initWithNibName:@"FlatView" bundle:nil];
break;
}
case TREE_VIEW:
{
//alloc/init
taskTableController = [[TaskTableController alloc] initWithNibName:@"TreeView" bundle:nil];
break;
}
}
//set bottom view?
self.bottomPaneBtn.selectedSegment = DYLIBS_VIEW;
//add subview
[self.topPane addSubview:self.taskTableController.view];
//disable autosize translations
self.taskTableController.view.translatesAutoresizingMaskIntoConstraints = NO;
//set frame
[[self.taskTableController view] setFrame:[self.topPane bounds]];
//constrain to parent
[self constrainView:self.topPane subView:self.taskTableController.view];
return;
}
//smartly reload a specific row in table
// ->arg determines pane (top/bottom) and for bottom pane, the active view the item belongs to
-(void)reloadRow:(id)item
{
//table view
__block NSTableView* tableView = nil;
//row
__block NSUInteger row = 0;
//run everything on main thread
// ensures table view isn't changed out from under us....
dispatch_async(dispatch_get_main_queue(), ^{
//top table (pane)
if(YES == [item isKindOfClass:[Task class]])
{
//top table view
tableView = [((id)self.taskTableController) itemView];
//reload item
// ->flat view
if(YES != [tableView isKindOfClass:[NSOutlineView class]])
{
//no filtering
// grab row from all tasks
if(YES != self.taskTableController.isFiltered)
{
//sync to get row
@synchronized (taskEnumerator.tasks)
{
//get row
row = [taskEnumerator.tasks indexOfKey:((Task*)item).pid];
}
}
//filtering
// grab row from filtered tasks
else
{
//sync to get row
@synchronized (self.taskTableController.filteredItems)
{
//get row
row = [self.taskTableController.filteredItems indexOfObject:item];
}
}
}
//reload item
// tree view, so no need to worry about filtering
else
{
//get row
row = [(NSOutlineView*)tableView rowForItem:item];
}
//sanity check
if(NSNotFound == row)
{
//bail
goto bail;
}
//begin updates
[tableView beginUpdates];
//reload row
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:(row)] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
//end updates
[tableView endUpdates];
}
//bottom pane
else
{
//bottom table view
tableView = [((id)self.bottomViewController) itemView];
//sync bottom view controller
@synchronized(self.bottomViewController)
{
//no filtering
// ->grab row from all items
if(YES != self.bottomViewController.isFiltered)
{
//get row
@synchronized(self.bottomViewController.tableItems)
{
//get
row = [self.bottomViewController.tableItems indexOfObject:item];
}
}
//filtering
// ->grab row from filtered item
else
{
//get row
@synchronized(self.bottomViewController.filteredItems)
{
//get row
row = [self.bottomViewController.filteredItems indexOfObject:item];
}
}
}//sync
//make sure item was found
if(NSNotFound == row)
{
//bail
goto bail;
}
//begin updates
[tableView beginUpdates];
//reload row
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:(row)] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
//end updates
[tableView endUpdates];
}//bottom pane
//bail
bail:
;
}); //dispatch on main thread
return;
}
//smartly, reload bottom pane on main thread
// ->checks if task & item type (e.g. files) are both selected
-(void)reloadBottomPane:(Task*)task itemView:(NSUInteger)itemView
{
//tag
__block NSUInteger segmentTag = 0;
//not found msg
NSString* noItemsMsg = nil;
//main thread
// get segment tag
if(YES == [NSThread isMainThread])
{
//get segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
}
//background thread
// get segment tag on main thread
else
{
//on main thread
// get segment tag
dispatch_sync(dispatch_get_main_queue(), ^{
//get segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
});
}
//ignore reloads for unselected tasks
// ->note: when no task is passed in, always reload though
if( (nil != task) &&
(self.currentTask != task))
{
//ignore
goto bail;
}
//ignore reloads for unselected views
if( (segmentTag != itemView) &&
(CURRENT_VIEW != itemView))
{
//ignore
goto bail;
}
//set input
switch(segmentTag)
{
//dylibs
case DYLIBS_VIEW:
//set table items
@synchronized(self.bottomViewController)
{
//set
self.bottomViewController.tableItems = self.currentTask.dylibs;
}
//if there is none
noItemsMsg = @"no dylibs found";
break;
//files
case FILES_VIEW:
//set table items
@synchronized(self.bottomViewController)
{
//set
self.bottomViewController.tableItems = self.currentTask.files;
}
//if there is none
noItemsMsg = @"no files found";
break;
//networking
case NETWORKING_VIEW:
//set table items
@synchronized(self.bottomViewController)
{
//set
self.bottomViewController.tableItems = self.currentTask.connections;
}
//if there is none
noItemsMsg = @"no network connections found";
break;
default:
break;
}
//execute on current thread
if(YES == [NSThread isMainThread])
{
//set not found label
self.noItemsLabel.stringValue = noItemsMsg;
//finalize
[self finalizeBottomReload];
}
else
{
//reload
// ->in main UI thread
dispatch_async(dispatch_get_main_queue(), ^{
//set not found label
self.noItemsLabel.stringValue = noItemsMsg;
//finalize
[self finalizeBottomReload];
});
}
//bail
bail:
return;
}
//finish bottom reload
-(void)finalizeBottomReload
{
//stop progress indicator
[self.bottomPaneSpinner stopAnimation:nil];
//hide
self.bottomPaneSpinner.hidden = YES;
//invoke refresh
[(id)self.bottomViewController reloadTable];
//no items?
// but there is something in top....
if( (0 == self.bottomViewController.tableItems.count) &&
(nil != self.currentTask) )
{
//how
self.noItemsLabel.hidden = NO;
}
//got items?
else
{
//hide
self.noItemsLabel.hidden = YES;
}
return;
}
//VT callback to reload a binary
// ->task binary: reload all instances that match in top pane
// dylib: reload row if dylib is loaded in current/selected task
-(void)reloadBinary:(Binary*)binary
{
//all tasks
OrderedDictionary* tasks = nil;
//task
Task* task = nil;
//task binary
// ->reload all instances that match in top pane
if(YES == binary.isTaskBinary)
{
//when not filtered
// ->use all tasks
if(YES != self.taskTableController.isFiltered)
{
//sync
@synchronized(taskEnumerator.tasks)
{
//get tasks
tasks = taskEnumerator.tasks;
//reload each row w/ new VT info
for(NSNumber* taskPid in tasks)
{
//extract task
task = tasks[taskPid];
//check for match
if(task.binary == binary)
{
//reload
[self reloadRow:task];
}
}
}//sync
}
//when filtered
// ->use filtered items
else
{
//sync
@synchronized(self.taskTableController.filteredItems)
{
//filtered items
for(Task* task in self.taskTableController.filteredItems)
{
//check for match
if(task.binary == binary)
{
//reload
[self reloadRow:task];
}
}
}//sync
}
}//top pane
//bottom pane
// ->can just invoke 'reloadRow' method (which has logic to handle filtering, ignoring 'not found' items, etc.)
else
{
//reload
[self reloadRow:binary];
}
//main thread
//update 'flagged' item
// ...method might be invoked by VT callback, etc
dispatch_async(dispatch_get_main_queue(), ^{
//flagged items?
// set icon to red
if(0 != taskEnumerator.flaggedItems.count)
{
//set main image
self.flaggedButton.image = [NSImage imageNamed:@"flaggedRed"];
//set alternate image
self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedRedBG"];
}
//no flagged items
// set icon back to default
else
{
//set main image
self.flaggedButton.image = [NSImage imageNamed:@"flagged"];
//set alternate image
self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedBG"];
}
});
return;
}
//display alert about OS not being supported
-(void)showUnsupportedAlert
{
//alert box
NSAlert* fullScanAlert = nil;
//alloc/init alert
fullScanAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"OS X %@ is not supported", [[NSProcessInfo processInfo] operatingSystemVersionString]] defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"sorry for the inconvenience!"];
//and show it
[fullScanAlert runModal];
return;
}
//init tracking areas for buttons
// ->provide mouse over effects
-(void)initTrackingAreas
{
//tracking area for buttons
NSTrackingArea* trackingArea = nil;
//init tracking area
// ->for 'refresh' button
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.refreshButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.refreshButton.tag]}];
//add tracking area to pref button
[self.refreshButton addTrackingArea:trackingArea];
//init tracking area
// ->for search button
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.searchButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.searchButton.tag]}];
//add tracking area to search button
[self.searchButton addTrackingArea:trackingArea];
//init tracking area
// ->for save button
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.saveButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.saveButton.tag]}];
//add tracking area to search button
[self.saveButton addTrackingArea:trackingArea];
//init tracking area
// ->for logo button
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.logoButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.logoButton.tag]}];
//add tracking area to logo button
[self.logoButton addTrackingArea:trackingArea];
//init tracking area
// ->for flagged items button
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.flaggedButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.flaggedButton.tag]}];
//add tracking area to flaggd items button
[self.flaggedButton addTrackingArea:trackingArea];
return;
}
//automatically invoked when user clicks logo
// ->load objective-see's html page
-(IBAction)logoButtonHandler:(id)sender
{
//open URL
// ->invokes user's default browser
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://objective-see.com"]];
return;
}
//sort tasks
// ->just by name (for flat view)
-(void)sortTasksForView:(OrderedDictionary*)tasks;
{
//sort tasks
// ->flat view, sort by name
if(FLAT_VIEW == self.taskViewFormat)
{
//sort
[tasks sort:SORT_BY_NAME];
}
return;
}
//reload task table
// invoke custom refresh method on main thread
-(void)reloadTaskTable
{
//sort tasks
[self sortTasksForView:taskEnumerator.tasks];
//when exec'ing on background thread
// ->exec on main thread
if(YES != [NSThread isMainThread])
{
//refresh on main thread
dispatch_async(dispatch_get_main_queue(), ^{
//refresh
[(id)self.taskTableController refresh];
});
}
//on main thread
// ->just refresh
else
{
//refresh
[(id)self.taskTableController refresh];
}
return;
}
//automatically invoked when window is closing
// ->terminate app
-(void)windowWillClose:(NSNotification *)notification
{
//exit
[NSApp terminate:self];
return;
}
//automatically invoked when mouse entered
-(void)mouseEntered:(NSEvent*)theEvent
{
//mouse entered
// ->highlight (visual) state
[self buttonAppearance:theEvent shouldReset:NO];
return;
}
//automatically invoked when mouse exits
-(void)mouseExited:(NSEvent*)theEvent
{
//mouse exited
// ->so reset button to original (visual) state
[self buttonAppearance:theEvent shouldReset:YES];
return;
}
//set or unset button's highlight
-(void)buttonAppearance:(NSEvent*)theEvent shouldReset:(BOOL)shouldReset
{
//tag
NSUInteger tag = 0;
//image name
NSString* imageName = nil;
//button
NSButton* button = nil;
//extract tag
tag = [((NSDictionary*)theEvent.userData)[@"tag"] unsignedIntegerValue];
//restore button back to default (visual) state
if(YES == shouldReset)
{
//set original refresh image
if(REFRESH_BUTTON_TAG == tag)
{
//set
imageName = @"refreshIcon";
}
//set original search image
else if(SEARCH_BUTTON_TAG == tag)
{
//set
imageName = @"search";
}
//set original save image
else if(SAVE_BUTTON_TAG == tag)
{
//set
imageName = @"saveIcon";
}
//set original logo image
else if(LOGO_BUTTON_TAG == tag)
{
//set
imageName = @"logoApple";
}
//set original flagged items image
// ->but also handle case where there are still flagged items
else if(FLAGGED_BUTTON_TAG == tag)
{
//when no flagged items
if(0 == taskEnumerator.flaggedItems.count)
{
//set
imageName = @"flagged";
}
//flagged items
else
{
//set
imageName = @"flaggedRed";
}
}
}
//highlight button
else
{
//set original refresh image
if(REFRESH_BUTTON_TAG == tag)
{
//set
imageName = @"refreshIconOver";
}
//set mouse over search image
else if(SEARCH_BUTTON_TAG == tag)
{
//set
imageName = @"searchOver";
}
//set mouse over 'save' image
else if(SAVE_BUTTON_TAG == tag)
{
//set
imageName = @"saveIconOver";
}
//set mouse over logo image
else if(LOGO_BUTTON_TAG == tag)
{
//set
imageName = @"logoAppleOver";
}
//set mouse over flagged items image
// ->also handles case where flagged items are present
else if(FLAGGED_BUTTON_TAG == tag)
{
//when no flagged items
if(0 == taskEnumerator.flaggedItems.count)
{
//set
imageName = @"flaggedOver";
}
//flagged items
else
{
//set
imageName = @"flaggedRedOver";
}
}
}
//set image
//grab button
button = [[[self window] contentView] viewWithTag:tag];
if(YES != [button isKindOfClass:[NSButton class]])
{
//wtf
goto bail;
}
//when enabled
// ->set image
if(YES == [button isEnabled])
{
//set
[button setImage:[NSImage imageNamed:imageName]];
}
//bail
bail:
return;
}
//invoked when user clicks 'save' icon
// ->show popup that allows user to save results
-(IBAction)saveResults:(id)sender
{
//save panel
NSSavePanel *panel = nil;
//save results popup
__block NSAlert* saveResultPopup = nil;
//output
// ->json of all tasks/dylibs, etc
__block NSMutableString* output = nil;
//error
__block NSError* error = nil;
//create panel
panel = [NSSavePanel savePanel];
//suggest file name
[panel setNameFieldStringValue:@"tasks.json"];
//show panel
// ->completion handler will invoked when user clicks 'ok'
[panel beginWithCompletionHandler:^(NSInteger result)
{
//only need to handle 'ok'
if(NSFileHandlingPanelOKButton == result)
{
//alloc output JSON
output = [NSMutableString string];
//start JSON
[output appendString:@"{\"tasks:\":["];
//sync
@synchronized(taskEnumerator.tasks)
{
//get tasks
for(NSNumber* taskPid in taskEnumerator.tasks)
{
//append task JSON
[output appendFormat:@"{%@},", [taskEnumerator.tasks[taskPid] toJSON]];
}
}//sync
//remove last ','
if(YES == [output hasSuffix:@","])
{
//remove
[output deleteCharactersInRange:NSMakeRange([output length]-1, 1)];
}
//terminate list/output
[output appendString:@"]}"];
//save JSON to disk
// ->on error will show err msg in popup
if(YES != [output writeToURL:[panel URL] atomically:NO encoding:NSUTF8StringEncoding error:&error])
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: saving output to %s failed with %s", [[panel URL] fileSystemRepresentation], [[error description] UTF8String]);
//init popup w/ error msg
saveResultPopup = [NSAlert alertWithMessageText:@"ERROR: failed to save output" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"details: %@", error];
}
//happy
// ->set result msg
else
{
//init popup w/ msg
saveResultPopup = [NSAlert alertWithMessageText:@"Succesfully saved output" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"file: %s", [[panel URL] fileSystemRepresentation]];
}
//show popup
[saveResultPopup runModal];
}
}];
//bail
bail:
return;
}
//automatically invoked when user clicks 'search' button/ cmd+f hotkey
// ->perform global search
-(IBAction)search:(id)sender
{
//alloc/init flagged items window
if(nil == self.searchWindowController)
{
//alloc/init
searchWindowController = [[SearchWindowController alloc] initWithWindowNibName:@"SearchWindow"];
}
//init/prep it
[self.searchWindowController prepare];
//show it
[self.searchWindowController showWindow:self];
//reset search string
[self.searchWindowController.searchBox setStringValue:@""];
return;
}
#pragma mark Menu Handler(s) #pragma mark -
//automatically invoked when user clicks 'About/Info'
// ->show about window
-(IBAction)about:(id)sender
{
//alloc/init settings window
if(nil == self.aboutWindowController)
{
//alloc/init
aboutWindowController = [[AboutWindowController alloc] initWithWindowNibName:@"AboutWindow"];
}
//center window
[[self.aboutWindowController window] center];
//show it
[self.aboutWindowController showWindow:self];
return;
}
/*
//automatically invoked when user clicks gear icon
// ->show preferences
-(IBAction)showPreferences:(id)sender
{
//alloc/init settings window
if(nil == self.prefsWindowController)
{
//alloc/init
prefsWindowController = [[PrefsWindowController alloc] initWithWindowNibName:@"PrefsWindow"];
}
//show it
[self.prefsWindowController showWindow:self];
//invoke function in background that will make window modal
// ->waits until window is non-nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//make modal
makeModal(self.prefsWindowController);
});
return;
}
*/
/*
//automatically invoked when menu is clicked
// ->tell menu to disable 'Preferences' when scan is running
-(BOOL)validateMenuItem:(NSMenuItem *)item
{
//enable
BOOL bEnabled = YES;
//check if item is 'Preferences'
if(PREF_MENU_ITEM_TAG == item.tag)
{
//unset enabled flag if scan is running
if(YES != [[self.scanButtonLabel stringValue] isEqualToString:START_SCAN])
{
//disable
bEnabled = NO;
}
}
return bEnabled;
}
*/
//automatically invoked when user clicks on Flat/Tree view
// ->invoke helper function to change view
-(IBAction)switchView:(id)sender
{
//ignore same selection
if(self.taskViewFormat == [[sender selectedCell] tag])
{
//bail
goto bail;
}
//save selected view
self.taskViewFormat = [[sender selectedCell] tag];
//flat view
// ->enable 'filter tasks' search field
if(FLAT_VIEW == self.taskViewFormat)
{
//enable
self.filterTasksBox.enabled = YES;
}
//tree view
// ->disable 'filter tasks' search field
else
{
//disable
self.filterTasksBox.enabled = NO;
}
//switch (top) view/pane
[self changeViewController];
//always unset filter flag
self.taskTableController.isFiltered = NO;
//always reset filter text
[self.filterTasksBox setStringValue:@""];
//sync
@synchronized(self.taskTableController.filteredItems)
{
//remove all filtered tasks
[self.taskTableController.filteredItems removeAllObjects];
}//sync
//reset current task
self.currentTask = nil;
//reload top pane
[self reloadTaskTable];
//select top row
[self.taskTableController.itemView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
//reload bottom pane
// ->ensures correct info is shown for selected (top) task
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) selectBottomPaneContent:nil];
//bail
bail:
return;
}
//(automatically) invoked on segmented button click or (manually) on task (top pane) switch
// ->change input (for pane) & then refresh it
-(IBAction)selectBottomPaneContent:(id)sender
{
//tag
NSUInteger segmentTag = 0;
//filter box placeholder text
NSString* filterPlaceholder = nil;
//always reset filter flag
self.bottomViewController.isFiltered = NO;
//always reset filter box's text
self.filterItemsBox.stringValue = @"";
//remove all filtered items
[self.bottomViewController.filteredItems removeAllObjects];
//for currently selected tasks
// ->check if its still alive
if(nil != self.currentTask)
{
//helper method that takes care of all check/handling dead tasks
[self.taskTableController handleRowSelection];
}
//when no current task
// set to first task in sorted tasks, unless filtering
if(nil == self.currentTask)
{
if( (YES != self.taskTableController.isFiltered) ||
( (YES == self.taskTableController.isFiltered) && (0 != self.taskTableController.filteredItems.count) ) )
{
//set to first
self.currentTask = taskEnumerator.tasks[[taskEnumerator.tasks keyAtIndex:0]];
}
}
//get segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
//always hide 'no items' label
self.noItemsLabel.hidden = YES;
//unset existing items
@synchronized(self.bottomViewController)
{
//unset
self.bottomViewController.tableItems = nil;
}
//when in a background thread
// ->perform UI stuff on main thread
if(YES != [NSThread isMainThread])
{
//refresh on main thread
dispatch_sync(dispatch_get_main_queue(), ^{
//reload to clear
[(id)self.bottomViewController reloadTable];
//show spinner
self.bottomPaneSpinner.hidden = NO;
//start progress indicator
[self.bottomPaneSpinner startAnimation:nil];
});
}
//in main thread already
// ->just perform UI actions directly
else
{
//reload to clear
[(id)self.bottomViewController reloadTable];
//start spinner
self.bottomPaneSpinner.hidden = NO;
//start progress indicator
[self.bottomPaneSpinner startAnimation:nil];
}
//set input
switch(segmentTag)
{
//dylibs
case DYLIBS_VIEW:
//init placeholder text for dylibs
filterPlaceholder = @"Filter Dylibs";
//remove all task's dylibs
[self.currentTask.dylibs removeAllObjects];
//skip kernel
if(0 == self.currentTask.pid.intValue)
{
//skip
break;
}
//(re)enumerate dylibs via XPC
// ->triggers table reload when done
[self.currentTask enumerateDylibs:taskEnumerator.dylibs shouldWait:NO];
break;
//files
case FILES_VIEW:
//init placeholder text for files
filterPlaceholder = @"Filter Files";
//remove all task's dylibs
[self.currentTask.files removeAllObjects];
//skip kernel
if(0 == self.currentTask.pid.intValue)
{
//skip
break;
}
//(re)enumerate files via XPC
// ->triggers table reload when done
[self.currentTask enumerateFiles:NO];
break;
//networking
case NETWORKING_VIEW:
//init placeholder text for network
filterPlaceholder = @"Filter Connections";
//skip kernel
if(0 == self.currentTask.pid.intValue)
{
//skip
break;
}
//(re)enumerate network connections via XPC
// ->triggers table reload when done
[self.currentTask enumerateNetworking:NO];
break;
default:
break;
}
//when in a background thread
// ->perform UI stuff on main thread
if(YES != [NSThread isMainThread])
{
//set filter box's placeholder text
// ->UI change, so do on main thread
dispatch_sync(dispatch_get_main_queue(), ^{
//set placeholder
[[self.filterItemsBox cell] setPlaceholderString:filterPlaceholder];
//kernel
// ->didn't enum so reload
if(0 == self.currentTask.pid.intValue)
{
//reload
[self reloadBottomPane:self.currentTask itemView:segmentTag];
}
});
}
//in main thread already
// ->just perform UI actions directly
else
{
//set placeholder
[[self.filterItemsBox cell] setPlaceholderString:filterPlaceholder];
//kernel
// ->didn't enum so reload
if(0 == self.currentTask.pid.intValue)
{
//reload
[self reloadBottomPane:self.currentTask itemView:segmentTag];
}
}
//bail
bail:
return;
}
//automatically invoked when user enters text in filter search boxes
// ->filter tasks and/or items
-(void)controlTextDidChange:(NSNotification *)aNotification
{
//search text
NSTextView* search = nil;
//tag for bottom pane selector
NSUInteger segmentTag = 0;
//extract search (text) view
search = aNotification.userInfo[@"NSFieldEditor"];
//sanity check
if(nil == search)
{
//bail
goto bail;
}
//prevent calling "complete" too often
if( (YES != self.completePosting) &&
(YES != self.commandHandling) )
{
//set flag
self.completePosting = YES;
//invoke complete
[aNotification.userInfo[@"NSFieldEditor"] complete:nil];
//unset flag
self.completePosting = NO;
}
//top pane
if(YES == [aNotification.object isEqualTo:self.filterTasksBox])
{
//when text is reset
// ->just reset flag
if(0 == search.string.length)
{
//set flag
self.taskTableController.isFiltered = NO;
}
//filter tasks
else
{
//'#' indicates a keyword search
// ->this is handled by customized auto-complete logic, so ignore
if(YES == [search.string hasPrefix:@"#"])
{
//ignore
goto bail;
}
//sync
@synchronized(self.taskTableController.filteredItems)
{
//remove prev
[self.taskTableController.filteredItems removeAllObjects];
//normal filter
[self.filterObj filterTasks:search.string items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP];
}//sync
//set flag
self.taskTableController.isFiltered = YES;
}
//finalize filtering/search
// ->updates UI, etc
[self finalizeFiltration:PANE_TOP];
}
//bottom pane
else if(YES == [aNotification.object isEqualTo:self.filterItemsBox])
{
//when text is reset
// ->just reset flag
if(0 == search.string.length)
{
//set flag
self.bottomViewController.isFiltered = NO;
}
//filter items
else
{
//'#' indicates a keyword search
// ->this is handled by customized auto-complete logic, so ignore
if(YES == [search.string hasPrefix:@"#"])
{
//ignore
goto bail;
}
//get segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
//remove all filtered tasks
[self.bottomViewController.filteredItems removeAllObjects];
//get item
// ->will bail if item isn't in (current) view, etc
switch(segmentTag)
{
//dylibs
case DYLIBS_VIEW:
{
//normal filter
[self.filterObj filterFiles:search.string items:self.currentTask.dylibs results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
break;
}
//files
case FILES_VIEW:
{
//filter
[self.filterObj filterFiles:search.string items:self.currentTask.files results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
break;
}
//network connections
case NETWORKING_VIEW:
{
//filter
[self.filterObj filterConnections:search.string items:self.currentTask.connections results:self.bottomViewController.filteredItems];
break;
}
default:
break;
}//switch
//set flag
self.bottomViewController.isFiltered = YES;
}
//finalize filtering/searching
// ->updates UI, etc
[self finalizeFiltration:PANE_BOTTOM];
}
//bail
bail:
return;
}
//config/show the filter overlay
// ->also disable all inputs/buttons
-(void)prepUIForFiltering:(NSUInteger)pane
{
//pre-req
[self.filteringOverlay setWantsLayer:YES];
//rounded corners
self.filteringOverlay.layer.cornerRadius = 20.0;
//maks
self.filteringOverlay.layer.masksToBounds = YES;
//dark mode
// set overlay to light
if(YES == isDarkMode())
{
//light gray
self.filteringOverlay.layer.backgroundColor = NSColor.lightGrayColor.CGColor;
}
//light mode
// set overlay to gray
else
{
//gray
self.filteringOverlay.layer.backgroundColor = NSColor.grayColor.CGColor;
}
//make it semi-transparent
self.filteringOverlay.alphaValue = 0.95;
//show overlay
self.filteringOverlay.hidden = NO;
//set text
// top pane
if(PANE_TOP == pane)
{
//set text
self.filteringMessage.stringValue = [NSString stringWithFormat:@"filtering tasks (%@)", self.filterTasksBox.stringValue];
}
//set text
// bottom pane
else
{
//set text
self.filteringMessage.stringValue = [NSString stringWithFormat:@"filtering dylibs (%@)", self.filterItemsBox.stringValue];
}
//start spinner
[self.filteringIndicator startAnimation:nil];
//disable view switcher
self.viewSelector.enabled = NO;
//disable filter box
self.filterTasksBox.enabled = NO;
//disable bottom pane switcher
self.bottomPaneBtn.enabled = NO;
//disable bottom filter item box
self.filterItemsBox.enabled = NO;
//disable refresh
self.refreshButton.enabled = NO;
//disable search
self.searchButton.enabled = NO;
//disable save
self.saveButton.enabled = NO;
//disable flagged
self.flaggedButton.enabled = NO;
//disable task table
self.taskTableController.itemView.enabled = NO;
//disable item table
self.bottomViewController.itemView.enabled = NO;
return;
}
//roll back changes to UI due to filtering
// ->hide overlay and enable all inputs/buttons
-(void)unprepUIForFiltering
{
//always (attempt to) hide filter overlay
self.filteringOverlay.hidden = YES;
//stop spinner
[self.filteringIndicator stopAnimation:nil];
//enable view switcher
self.viewSelector.enabled = YES;
//enable filter box
self.filterTasksBox.enabled = YES;
//enable bottom pane switcher
self.bottomPaneBtn.enabled = YES;
//enable bottom filter item box
self.filterItemsBox.enabled = YES;
//enable refresh
self.refreshButton.enabled = YES;
//enable search
self.searchButton.enabled = YES;
//enable save
self.saveButton.enabled = YES;
//enable flagged
self.flaggedButton.enabled = YES;
//enable task table
self.taskTableController.itemView.enabled = YES;
//enable item table
self.bottomViewController.itemView.enabled = YES;
return;
}
//code to complete filtering/search
// ->reload table/scroll to top etc
-(void)finalizeFiltration:(NSUInteger)pane
{
//hide overlay, etc
[self unprepUIForFiltering];
//top pane (task)
if(PANE_TOP == pane)
{
//always reload task (top) pane
// ->will trigger bottom load too
[self.taskTableController.itemView reloadData];
//scroll to top
[self.taskTableController scrollToTop];
//when nothing matches
// ->reset current task and bottom pane
if( (YES == self.taskTableController.isFiltered) &&
(0 == self.taskTableController.filteredItems.count) )
{
//unset bottom pane's items
@synchronized(self.bottomViewController)
{
//unset
self.bottomViewController.tableItems = nil;
}
//reset current task
self.currentTask = nil;
//stop progress indicator
[self.bottomPaneSpinner stopAnimation:nil];
//reload bottom pane
[self.bottomViewController.itemView reloadData];
}
}
//bottom pane (dylibs, files, etc)
else if(PANE_BOTTOM == pane)
{
//always reload item (bottom) pane
[self.bottomViewController.itemView reloadData];
//scroll to top
[self.bottomViewController scrollToTop];
}
return;
}
//action for 'refresh' button/cmd+r hotkey
// ->query OS to refresh/reload all tasks
-(IBAction)refreshTasks:(id)sender
{
//unselect current task
self.currentTask = nil;
//unset filter flag
self.taskTableController.isFiltered = NO;
//sync
@synchronized(self.taskTableController.filteredItems)
{
//remove all filtered items
[self.taskTableController.filteredItems removeAllObjects];
}
//reset filter box
self.filterTasksBox.stringValue = @"";
//reset segment (bottom pane) back to dylibs
self.bottomPaneBtn.selectedSegment = DYLIBS_VIEW;
//scroll to top
[self.taskTableController scrollToTop];
//select top row
[self.taskTableController.itemView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
//wait till (existing) task enumerator thread is done
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//wait
while(YES == [taskEnumerator.enumerator isExecuting])
{
//nap
[NSThread sleepForTimeInterval:0.5f];
}
//(re)explore tasks
[self exploreTasks];
});
return;
}
//constrain subview to parent view
-(void)constrainView:(NSView*)containerView subView:(NSView*)subView
{
//remove top constraint
if(nil != self.topConstraint)
{
//remove
[containerView removeConstraint:self.topConstraint];
}
//remove bottom constraint
if(nil != self.bottomConstraint)
{
//remove
[containerView removeConstraint:self.bottomConstraint];
}
//remove leading constraint
if(nil != self.leadingConstraint)
{
//remove
[containerView removeConstraint:self.leadingConstraint];
}
//remove trailing constraint
if(nil != self.trailingConstraint)
{
//remove
[containerView removeConstraint:self.trailingConstraint];
}
//create top constraint
self.topConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0];
//add top constraint
[containerView addConstraint:self.topConstraint];
//create bottom constraint
self.bottomConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0];
//add bottom constraint
[containerView addConstraint:self.bottomConstraint];
//create leading constraint
self.leadingConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0];
//add leading constraint
[containerView addConstraint:self.leadingConstraint];
//create trailing constraint
self.trailingConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0];
//add trailing constraint
[containerView addConstraint:self.trailingConstraint];
return;
}
//button handle for 'flagged items' button
// ->display (in separate popup) all flagged items
-(IBAction)showFlaggedItems:(id)sender
{
//alert box
NSAlert* alert = nil;
//handle case where there aren't any flagged items
// ->just show alert
if(0 == taskEnumerator.flaggedItems.count)
{
//alloc/init alert
alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"No items flagged by VirusTotal"] defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"hooray! 😇"];
//and show it
[alert runModal];
}
//show flagged items
else
{
//alloc/init flagged items window
if(nil == self.flagItemsWindowController)
{
//alloc/init
flagItemsWindowController = [[FlaggedItems alloc] initWithWindowNibName:@"FlaggedItems"];
}
//init/prep it
[self.flagItemsWindowController prepare];
//show it
[self.flagItemsWindowController showWindow:self];
/*
//invoke function in background that will make window modal
// ->waits until window is non-nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//make modal
makeModal(self.flagItemsWindowController);
});
*/
}
return;
}
//delegate method, automatically called
// ->generate list of matches to return for drop-down
-(NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
{
//matches
NSMutableArray *matches = nil;
//range options
NSUInteger rangeOptions = {0};
//segment tag
NSUInteger segmentTag = 0;
//init array for matches
matches = [[NSMutableArray alloc] init];
//init range options
rangeOptions = NSAnchoredSearch | NSCaseInsensitiveSearch;
//grab segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
//for now, only filter binaries
// ->top pane: any (well, just tasks)
// bottom pane: only dylibs
if( (textView != self.customTasksFilter) &&
(DYLIBS_VIEW != segmentTag) )
{
//bail
goto bail;
}
//check all filters
for(NSString* filter in self.filterObj.binaryFilters)
{
//check if found
// ->add to match when found
if([filter rangeOfString:textView.string options:rangeOptions range:NSMakeRange(0, filter.length)].location != NSNotFound)
{
//add
[matches addObject:filter];
}
}
//sort matches
[matches sortUsingComparator:^(NSString *a, NSString *b)
{
//sort
return [a localizedStandardCompare:b];
}];
//bail
bail:
return matches;
}
//delegate method, automatically invoked
// ->handle invocations for text view
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
//flag
BOOL didPerformRequestedSelectorOnTextView = NO;
//invocation
NSInvocation *textViewInvocationForSelector = nil;
//check if text view can handle selector
if(YES != [textView respondsToSelector:commandSelector])
{
//bail
goto bail;
}
//set iVar flag
self.commandHandling = YES;
//handle 'enter'
// ->but only for #'s
if( (commandSelector == @selector(insertNewline:)) &&
(YES == [filterObj isKeyword:textView.string]) )
{
//filter
[self filterAutoComplete:textView];
//unset iVar
self.commandHandling = NO;
//set flag
didPerformRequestedSelectorOnTextView = YES;
}
//just invoke system handler for rest...
else
{
//init invocation
textViewInvocationForSelector = [NSInvocation invocationWithMethodSignature:[textView methodSignatureForSelector:commandSelector]];
//set target
[textViewInvocationForSelector setTarget:textView];
//set selector
[textViewInvocationForSelector setSelector:commandSelector];
//invoke selector
[textViewInvocationForSelector invoke];
//unset iVar
self.commandHandling = NO;
//indicate that selector was performed
didPerformRequestedSelectorOnTextView = YES;
}
bail:
return didPerformRequestedSelectorOnTextView;
}
//callback for custom search fields
// handles the auto-complete filterings
-(void)filterAutoComplete:(NSTextView*)textView
{
//filter string
NSString* filterString = nil;
//tag
NSUInteger segmentTag = 0;
//alert box
NSAlert* noKeywordsAlert = nil;
//extract filter
filterString = textView.textStorage.string;
//handle top pane (tasks)
if(textView == self.customTasksFilter)
{
//set flag
self.taskTableController.isFiltered = YES;
//sync
@synchronized(self.taskTableController.filteredItems)
{
//remove all filtered tasks
[self.taskTableController.filteredItems removeAllObjects];
//update ui
[self finalizeFiltration:PANE_TOP];
//filter
[self.filterObj filterTasks:filterString items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP];
}
//refresh/update UI when not a keyword search
// ->keyword search are done in background, and will call this directly when pau
if(YES != [filterString hasPrefix:@"#"])
{
//finalize filtering
[self finalizeFiltration:PANE_TOP];
}
}
//handle bottom pane
// ->just dylibs
else if(textView == self.customItemsFilter)
{
//get segment tag
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
//check if keyword was entered for files/networks
if(segmentTag != DYLIBS_VIEW)
{
//alloc/init alert
noKeywordsAlert = [NSAlert alertWithMessageText:@"#keywords not (yet) supported for files/connections" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@""];
//and show it
[noKeywordsAlert runModal];
//reset search
self.filterItemsBox.stringValue = @"";
//bail
goto bail;
}
//set flag
self.bottomViewController.isFiltered = YES;
@synchronized(self.bottomViewController.filteredItems)
{
//remove all filtered tasks
[self.bottomViewController.filteredItems removeAllObjects];
//update ui
[self finalizeFiltration:PANE_BOTTOM];
//filter
[self.filterObj filterFiles:filterString items:self.currentTask.dylibs results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
}
//refresh/update UI when not a keyword search
// ->keyword search are done in background, and will call this directly when pau
if(YES != [filterString hasPrefix:@"#"])
{
//finalize filtering
[self finalizeFiltration:PANE_BOTTOM];
}
}
//bail
bail:
return;
}
//automatically invoked
// ->set all NSSearchFields to be instances of our custom NSTextView
-(id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
{
//field editor
id fieldEditor = nil;
//ignore non-NSSearchField classes
if(YES != [client isKindOfClass:[NSSearchField class]])
{
//ingnore
goto bail;
}
//set task's filter search field
if(client == self.filterTasksBox)
{
//assign for return
fieldEditor = self.customTasksFilter;
}
//set item's filter search field
else if(client == self.filterItemsBox)
{
//assign for return
fieldEditor = self.customItemsFilter;
}
//bail
bail:
return fieldEditor;
}
//call into Update obj
// check to see if there an update?
- (IBAction)check4Update:(id)sender
{
//update obj
Update* update = nil;
//init update obj
update = [[Update alloc] init];
//check for update
// ->'updateResponse newVersion:' method will be called when check is done
[update checkForUpdate:^(NSUInteger result, NSString* newVersion) {
//process response
// when invoked from menu show full results
[self updateResponse:result newVersion:newVersion alwaysShow:(nil != sender)];
}];
return;
}
//process update response
// error, no update, update/new version
-(void)updateResponse:(NSInteger)result newVersion:(NSString*)newVersion alwaysShow:(BOOL)alwaysShow
{
//details
NSString* details = nil;
//action
NSString* action = nil;
//handle response
// new version, show popup
switch(result)
{
//error
case UPDATE_ERROR:
//set details
details = @"error, failed to check for an update.";
//set action
action = @"Close";
break;
//no updates
case UPDATE_NOTHING_NEW:
//set details
details = [NSString stringWithFormat:@"you're all up to date! (v. %@)", getAppVersion()];
//set action
action = @"Close";
break;
//new version
case UPDATE_NEW_VERSION:
//set details
details = [NSString stringWithFormat:@"a new version (%@) is available!", newVersion];
//set action
action = @"Update";
break;
}
//new version?
//...or always show results?
if( (YES == alwaysShow) ||
(UPDATE_NEW_VERSION == result) )
{
//alloc update window
updateWindowController = [[UpdateWindowController alloc] initWithWindowNibName:@"UpdateWindow"];
//configure
[self.updateWindowController configure:details buttonTitle:action];
//center window
[[self.updateWindowController window] center];
//show it
[self.updateWindowController showWindow:self];
//invoke function in background that will make window modal
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//make modal
makeModal(self.updateWindowController);
});
}
return;
}
@end