diff --git a/Application/Application/AppDelegate.m b/Application/Application/AppDelegate.m index a598118..61ef146 100644 --- a/Application/Application/AppDelegate.m +++ b/Application/Application/AppDelegate.m @@ -371,21 +371,28 @@ bail: switch (result) { //error - case -1: + case Update_Error: //err msg os_log_error(logHandle, "ERROR: update check failed"); break; //no updates - case 0: + case Update_None: //dbg msg os_log_debug(logHandle, "no updates available"); break; + //this version of macOS, not supported + case Update_NotSupported: + + //dbg msg + os_log_debug(logHandle, "update available, but not for this version of macOS"); + break; + //new version - case 1: + case Update_Available: //dbg msg os_log_debug(logHandle, "a new version (%@) is available", newVersion); diff --git a/Application/Application/Preferences.xib b/Application/Application/Preferences.xib index 6a194d8..8462d8f 100644 --- a/Application/Application/Preferences.xib +++ b/Application/Application/Preferences.xib @@ -1,14 +1,15 @@ - + - + + @@ -25,7 +26,7 @@ - + @@ -41,21 +42,21 @@ - + - + - + @@ -197,25 +198,16 @@ - + - + - - - - - - - - - - + @@ -235,7 +227,7 @@ - + @@ -245,9 +237,9 @@ - + - + @@ -256,7 +248,35 @@ + + + + + + + + + + + + + + + @@ -303,21 +323,21 @@ - - + + - + + + + + - - - - - + diff --git a/Application/Application/PrefsWindowController.h b/Application/Application/PrefsWindowController.h index 0b2492d..bd43166 100644 --- a/Application/Application/PrefsWindowController.h +++ b/Application/Application/PrefsWindowController.h @@ -47,6 +47,9 @@ //path to action @property (weak) IBOutlet NSTextField *executePath; +//browse button +@property (weak) IBOutlet NSButton *browseButton; + //execute args button @property (weak) IBOutlet NSButton *executeArgsButton; diff --git a/Application/Application/PrefsWindowController.m b/Application/Application/PrefsWindowController.m index c40418d..f2362c9 100644 --- a/Application/Application/PrefsWindowController.m +++ b/Application/Application/PrefsWindowController.m @@ -97,9 +97,9 @@ extern os_log_t logHandle; break; - //modes + //actions case TOOLBAR_ACTION: - + { //set view view = self.actionView; @@ -112,7 +112,7 @@ extern os_log_t logHandle; //set self.executePath.stringValue = [NSUserDefaults.standardUserDefaults objectForKey:PREF_EXECUTE_PATH]; } - + //set state of 'execute action' to match self.executePath.enabled = [NSUserDefaults.standardUserDefaults boolForKey:PREF_EXECUTE_ACTION]; @@ -121,9 +121,19 @@ extern os_log_t logHandle; //set state of 'execute action' to match self.executeArgsButton.enabled = [NSUserDefaults.standardUserDefaults boolForKey:PREF_EXECUTE_ACTION]; - - break; + //make 'Browse' button first responder + // calling this without a timeout, sometimes fails :/ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), + ^{ + + //set first responder + [self.window makeFirstResponder:self.browseButton]; + + }); + + break; + } //update case TOOLBAR_UPDATE: @@ -153,26 +163,6 @@ bail: return; } -//automatically called when 'enter' is hit -// save values that were entered in text field --(void)controlTextDidEndEditing:(NSNotification *)notification -{ - //execute path? - if([notification object] != self.executePath) - { - //bail - goto bail; - } - - //save & sync - [NSUserDefaults.standardUserDefaults setObject:self.executePath.stringValue forKey:PREF_EXECUTE_PATH]; - [NSUserDefaults.standardUserDefaults synchronize]; - -bail: - - return; -} - //invoked when user toggles button // update preferences for that button -(IBAction)togglePreference:(id)sender @@ -237,6 +227,15 @@ bail: //set path field state to match self.executeArgsButton.enabled = state; + //enabled, but no path? + // launch 'browse' pane for user to select + if( (NSControlStateValueOn == state) && + (0 == self.executePath.stringValue.length) ) + { + //show 'browse' + [self browseButtonHandler:nil]; + } + break; } @@ -277,6 +276,7 @@ bail: return; } +//TODO: supported OS //'check for update' button handler -(IBAction)check4Update:(id)sender { @@ -308,7 +308,7 @@ bail: } //process update response -// error, no update, update/new version +// error, no update, update not compatible, update/new version -(void)updateResponse:(NSInteger)result newVersion:(NSString*)newVersion { //re-enable button @@ -320,15 +320,15 @@ bail: switch(result) { //error - case -1: + case Update_Error: //set label - self.updateLabel.stringValue = @"error: update check failed"; + self.updateLabel.stringValue = @"Error: update check failed"; break; //no updates - case 0: + case Update_None: //dbg msg os_log_debug(logHandle, "no updates available"); @@ -337,10 +337,20 @@ bail: self.updateLabel.stringValue = [NSString stringWithFormat:@"Installed version (%@),\r\nis the latest.", getAppVersion()]; break; - + //this version of macOS, not supported + case Update_NotSupported: + + //dbg msg + os_log_debug(logHandle, "update available, but not for this version of macOS"); + + //set label + self.updateLabel.stringValue = [NSString stringWithFormat:@"Update available, but isn't supported on macOS %ld.%ld", NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, NSProcessInfo.processInfo.operatingSystemVersion.minorVersion]; + + break; + //new version - case 1: + case Update_Available: //dbg msg os_log_debug(logHandle, "a new version (%@) is available", newVersion); @@ -391,4 +401,54 @@ bail: return; } +//'browse' button handler +// open a panel for user to select file +-(IBAction)browseButtonHandler:(id)sender +{ + //'browse' panel + NSOpenPanel *panel = nil; + + //response to 'browse' panel + NSInteger response = 0; + + //init panel + panel = [NSOpenPanel openPanel]; + + //allow files + panel.canChooseFiles = YES; + + //don't allow directories + panel.canChooseDirectories = NO; + + //disable multiple selections + panel.allowsMultipleSelection = NO; + + //can open app bundles + panel.treatsFilePackagesAsDirectories = YES; + + //start in user's home directory + panel.directoryURL = [NSURL fileURLWithPath:NSHomeDirectory()]; + + //show it + response = [panel runModal]; + + //ignore cancel + if(NSModalResponseCancel == response) + { + //bail + goto bail; + } + + //set path in ui + self.executePath.stringValue = panel.URL.path; + + //save path & sync + [NSUserDefaults.standardUserDefaults setObject:self.executePath.stringValue forKey:PREF_EXECUTE_PATH]; + [NSUserDefaults.standardUserDefaults synchronize]; + +bail: + + return; +} + @end diff --git a/Application/Application/Update.m b/Application/Application/Update.m index 5b9ca43..f0805fe 100644 --- a/Application/Application/Update.m +++ b/Application/Application/Update.m @@ -25,25 +25,66 @@ extern os_log_t logHandle; // ->will invoke app delegate method to update UI when check completes -(void)checkForUpdate:(void (^)(NSUInteger result, NSString* latestVersion))completionHandler { - //latest version - __block NSString* latestVersion = nil; + //info + __block NSDictionary* productInfo = nil; //result - __block NSInteger result = -1; + __block NSInteger result = Update_None; //get latest version in background dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - //grab latest version - latestVersion = [self getLatestVersion]; - if(nil != latestVersion) + //latest version + NSString* latestVersion = nil; + + //supported OS + NSOperatingSystemVersion supportedOS = {0}; + + //get product info + productInfo = [self getProductInfo:PRODUCT_NAME]; + if(nil == productInfo) { - //check - result = (NSOrderedAscending == [getAppVersion() compare:latestVersion options:NSNumericSearch]); + //err msg + os_log_error(logHandle, "ERROR: failed retrieve product info (for update check) from %{public}@", PRODUCT_VERSIONS_URL); + + //error + result = Update_Error; + } + //got remote product info + // check supported OS and latest version + else + { + //init supported OS + supportedOS.majorVersion = [productInfo[SUPPORTED_OS_MAJOR] intValue]; + supportedOS.minorVersion = [productInfo[SUPPORTED_OS_MINOR] intValue]; + + //extract latest version + latestVersion = productInfo[LATEST_VERSION]; + + //supported version of macOS? + if(YES != [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:supportedOS]) + { + //dbg msg + os_log_debug(logHandle, "latest version requires macOS %ld.%ld ...but current macOS is %{public}@", supportedOS.majorVersion, supportedOS.minorVersion, NSProcessInfo.processInfo.operatingSystemVersionString); + + //unsupported + result = Update_NotSupported; + } + + //latest version is new(er)? + else if(nil != latestVersion) + { + //check app version and latest version + if(NSOrderedAscending == [getAppVersion() compare:latestVersion options:NSNumericSearch]) + { + //new update! + result = Update_Available; + } + } } //invoke app delegate method - // ->will update UI/show popup if necessart + // will update UI/show popup if necessary dispatch_async(dispatch_get_main_queue(), ^{ completionHandler(result, latestVersion); @@ -53,53 +94,27 @@ extern os_log_t logHandle; return; } -//query interwebz to get latest version --(NSString*)getLatestVersion +//read JSON file w/ products +// return dictionary w/ info about this product +-(NSDictionary*)getProductInfo:(NSString*)product { //product version(s) data - NSData* productsVersionData = nil; + NSDictionary* products = nil; - //version dictionary - NSDictionary* productsVersionDictionary = nil; - - //latest version - NSString* latestVersion = nil; - - //get version from remote URL - productsVersionData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:PRODUCT_VERSIONS_URL]]; - if(nil == productsVersionData) - { - //bail - goto bail; - } - - //convert JSON to dictionary - // ->wrap as may throw exception + //get json file (products) from remote URL @try { //convert - productsVersionDictionary = [NSJSONSerialization JSONObjectWithData:productsVersionData options:0 error:nil]; - if(nil == productsVersionDictionary) - { - //bail - goto bail; - } + products = [NSJSONSerialization JSONObjectWithData:[[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:PRODUCT_VERSIONS_URL]] options:0 error:nil]; } @catch(NSException* exception) { - //bail - goto bail; + ; } - //extract latest version - latestVersion = [[productsVersionDictionary objectForKey:@"OverSight"] objectForKey:@"version"]; - - //dbg msg - os_log_debug(logHandle, "latest version: %{public}@", latestVersion); - bail: - return latestVersion; + return products[product]; } @end diff --git a/Installer/Source/Configure.m b/Installer/Source/Configure.m index c58c41b..d0bb91c 100644 --- a/Installer/Source/Configure.m +++ b/Installer/Source/Configure.m @@ -441,6 +441,9 @@ bail: //path to preferences dir NSString* prefsDirectory = nil; + //preferences file + NSString* preferences = nil; + //path to login item NSURL* loginItem = nil; @@ -522,14 +525,25 @@ bail: } //full? - // delete prefs / allowed items + // delete preferences if(YES == full) { - //dbg msg - os_log_debug(logHandle, "removing preferences/allowed items"); + //init prefs file + preferences = [NSHomeDirectory() stringByAppendingPathComponent:PREFERENCES]; - //delete prefs via defaults - execTask(DEFAULTS, @[@"delete", @BUNDLE_ID], YES, NO); + //dbg msg + os_log_debug(logHandle, "deleting preferences"); + + //delete preferences file + // and if this fails, reset + if(YES != [NSFileManager.defaultManager removeItemAtPath:preferences error:nil]) + { + //dbg msg + os_log_debug(logHandle, "deleting failed, will reset preferences"); + + //reset + [NSUserDefaults.standardUserDefaults removePersistentDomainForName:@BUNDLE_ID]; + } } //always remove application diff --git a/Shared/Consts.h b/Shared/Consts.h index c1f8568..80960f8 100644 --- a/Shared/Consts.h +++ b/Shared/Consts.h @@ -70,6 +70,15 @@ //error(s) url #define ERRORS_URL @"https://objective-see.com/errors.html" +//os major +#define SUPPORTED_OS_MAJOR @"OSMajor" + +//os minor +#define SUPPORTED_OS_MINOR @"OSMinor" + +//latest version +#define LATEST_VERSION @"version" + //close category #define CATEGORY_CLOSE @"close" @@ -110,6 +119,9 @@ //add rule, allow #define BUTTON_ALLOW 1 +//preferences file +#define PREFERENCES @"/Library/Preferences/com.objective-see.oversight.plist" + //prefs // disabled status #define PREF_IS_DISABLED @"disabled" @@ -198,9 +210,6 @@ //path to launchctl #define LAUNCHCTL @"/bin/launchctl" -//path to defaults -#define DEFAULTS @"/usr/bin/defaults" - //path to kill all #define KILL_ALL @"/usr/bin/killall" @@ -237,6 +246,9 @@ //av devices typedef enum {Device_Camera, Device_Microphone} AVDevice; +//updates +typedef enum {Update_Error, Update_None, Update_NotSupported, Update_Available} UpdateStatus; + //log levels typedef enum {Log_Level_Default, Log_Level_Info, Log_Level_Debug} LogLevels;