Core/Source/GrowlPreferencesController.m
author Rudy Richter
Sat Aug 01 20:50:32 2009 -0400 (2009-08-01)
changeset 4261 48b7c994f6c8
parent 4246 4f52d1d98978
child 4270 5c1d8f5e64c2
permissions -rw-r--r--
PrefPane: clang warnings and setup for Sparkle
     1 //
     2 //  GrowlPreferencesController.m
     3 //  Growl
     4 //
     5 //  Created by Nelson Elhage on 8/24/04.
     6 //  Renamed from GrowlPreferences.m by Mac-arena the Bored Zo on 2005-06-27.
     7 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     8 //
     9 // This file is under the BSD License, refer to License.txt for details
    10 
    11 
    12 #import "GrowlPreferencesController.h"
    13 #import "GrowlDefinesInternal.h"
    14 #import "GrowlDefines.h"
    15 #import "GrowlPathUtilities.h"
    16 #import "NSStringAdditions.h"
    17 #include "CFURLAdditions.h"
    18 #include "CFDictionaryAdditions.h"
    19 #include "LoginItemsAE.h"
    20 #include <Security/SecKeychain.h>
    21 #include <Security/SecKeychainItem.h>
    22 
    23 #define keychainServiceName "Growl"
    24 #define keychainAccountName "Growl"
    25 
    26 CFTypeRef GrowlPreferencesController_objectForKey(CFTypeRef key) {
    27 	return [[GrowlPreferencesController sharedController] objectForKey:(id)key];
    28 }
    29 
    30 CFIndex GrowlPreferencesController_integerForKey(CFTypeRef key) {
    31 	Boolean keyExistsAndHasValidFormat;
    32 	return CFPreferencesGetAppIntegerValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
    33 }
    34 
    35 Boolean GrowlPreferencesController_boolForKey(CFTypeRef key) {
    36 	Boolean keyExistsAndHasValidFormat;
    37 	return CFPreferencesGetAppBooleanValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
    38 }
    39 
    40 unsigned short GrowlPreferencesController_unsignedShortForKey(CFTypeRef key)
    41 {
    42 	CFIndex theIndex = GrowlPreferencesController_integerForKey(key);
    43 	
    44 	if (theIndex > USHRT_MAX)
    45 		return USHRT_MAX;
    46 	else if (theIndex < 0)
    47 		return 0;
    48 	return (unsigned short)index;
    49 }
    50 
    51 @implementation GrowlPreferencesController
    52 
    53 + (GrowlPreferencesController *) sharedController {
    54 	return [self sharedInstance];
    55 }
    56 
    57 - (id) initSingleton {
    58 	if ((self = [super initSingleton])) {
    59 		[[NSDistributedNotificationCenter defaultCenter] addObserver:self
    60 															selector:@selector(growlPreferencesChanged:)
    61 																name:GrowlPreferencesChanged
    62 															  object:nil];
    63 	}
    64 	return self;
    65 }
    66 
    67 - (void) destroy {
    68 	[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
    69 
    70 	[super destroy];
    71 }
    72 
    73 #pragma mark -
    74 
    75 - (void) registerDefaults:(NSDictionary *)inDefaults {
    76 	NSUserDefaults *helperAppDefaults = [[NSUserDefaults alloc] init];
    77 	[helperAppDefaults addSuiteNamed:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
    78 	NSDictionary *existing = [helperAppDefaults persistentDomainForName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
    79 	if (existing) {
    80 		NSMutableDictionary *domain = [inDefaults mutableCopy];
    81 		[domain addEntriesFromDictionary:existing];
    82 		[helperAppDefaults setPersistentDomain:domain forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
    83 		[domain release];
    84 	} else {
    85 		[helperAppDefaults setPersistentDomain:inDefaults forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
    86 	}
    87 	[helperAppDefaults release];
    88 	SYNCHRONIZE_GROWL_PREFS();
    89 }
    90 
    91 - (id) objectForKey:(NSString *)key {
    92 	id value = (id)CFPreferencesCopyAppValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
    93 	if(value)
    94 		CFMakeCollectable(value);
    95 	return [value autorelease];
    96 }
    97 
    98 - (void) setObject:(id)object forKey:(NSString *)key {
    99 	CFPreferencesSetAppValue((CFStringRef)key,
   100 							 (CFPropertyListRef)object,
   101 							 (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
   102 
   103 	SYNCHRONIZE_GROWL_PREFS();
   104 
   105 	int pid = getpid();
   106 	CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
   107 	CFStringRef pidKey = CFSTR("pid");
   108 	CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&pidKey, (const void **)&pidValue, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
   109 	CFRelease(pidValue);
   110 	CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
   111 										 (CFStringRef)GrowlPreferencesChanged,
   112 										 /*object*/ key,
   113 										 /*userInfo*/ userInfo,
   114 										 /*deliverImmediately*/ false);
   115 	CFRelease(userInfo);
   116 }
   117 
   118 - (BOOL) boolForKey:(NSString *)key {
   119 	return GrowlPreferencesController_boolForKey((CFTypeRef)key);
   120 }
   121 
   122 - (void) setBool:(BOOL)value forKey:(NSString *)key {
   123 	NSNumber *object = [[NSNumber alloc] initWithBool:value];
   124 	[self setObject:object forKey:key];
   125 	[object release];
   126 }
   127 
   128 - (CFIndex) integerForKey:(NSString *)key {
   129 	return GrowlPreferencesController_integerForKey((CFTypeRef)key);
   130 }
   131 
   132 - (void) setInteger:(CFIndex)value forKey:(NSString *)key {
   133 #ifdef __LP64__
   134 	NSNumber *object = [[NSNumber alloc] initWithInteger:value];
   135 #else
   136 	NSNumber *object = [[NSNumber alloc] initWithInt:value];
   137 #endif
   138 	[self setObject:object forKey:key];
   139 	[object release];
   140 }
   141 
   142 - (unsigned short)unsignedShortForKey:(NSString *)key
   143 {
   144 	return GrowlPreferencesController_unsignedShortForKey((CFTypeRef)key);
   145 }
   146 
   147 
   148 - (void)setUnsignedShort:(unsigned short)theShort forKey:(NSString *)key
   149 {
   150 	[self setObject:[NSNumber numberWithUnsignedShort:theShort] forKey:key];
   151 }
   152 
   153 - (void) synchronize {
   154 	SYNCHRONIZE_GROWL_PREFS();
   155 }
   156 
   157 #pragma mark -
   158 #pragma mark Start-at-login control
   159 
   160 - (BOOL) shouldStartGrowlAtLogin {
   161 	OSStatus   status;
   162 	Boolean    foundIt = false;
   163 	CFArrayRef loginItems = NULL;
   164 
   165 	//get the prefpane bundle and find GHA within it.
   166 	NSString *pathToGHA      = [[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] pathForResource:@"GrowlHelperApp" ofType:@"app"];
   167 	//get the file url to GHA.
   168 	CFURLRef urlToGHA = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)pathToGHA, kCFURLPOSIXPathStyle, true);
   169 
   170 	status = LIAECopyLoginItems(&loginItems);
   171 	if (status == noErr) {
   172 		for (CFIndex i=0, count=CFArrayGetCount(loginItems); i<count; ++i) {
   173 			CFDictionaryRef loginItem = CFArrayGetValueAtIndex(loginItems, i);
   174 			foundIt = CFEqual(CFDictionaryGetValue(loginItem, kLIAEURL), urlToGHA);
   175 			if (foundIt)
   176 				break;
   177 		}
   178 		CFRelease(loginItems);
   179 	}
   180 
   181 	CFRelease(urlToGHA);
   182 
   183 	return foundIt;
   184 }
   185 
   186 - (void) setShouldStartGrowlAtLogin:(BOOL)flag {
   187 	//get the prefpane bundle and find GHA within it.
   188 	NSString *pathToGHA = [[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] pathForResource:@"GrowlHelperApp" ofType:@"app"];
   189 	[self setStartAtLogin:pathToGHA enabled:flag];
   190 }
   191 
   192 - (void) setStartAtLogin:(NSString *)path enabled:(BOOL)enabled {
   193 	OSStatus status;
   194 	CFArrayRef loginItems = NULL;
   195 	NSURL *url = [NSURL fileURLWithPath:path];
   196 	NSInteger existingLoginItemIndex = -1;
   197 
   198 	status = LIAECopyLoginItems(&loginItems);
   199 
   200 	if (status == noErr) {
   201 		NSEnumerator *enumerator = [(NSArray *)loginItems objectEnumerator];
   202 		NSDictionary *loginItemDict;
   203 
   204 		while ((loginItemDict = [enumerator nextObject])) {
   205 			if ([[loginItemDict objectForKey:(NSString *)kLIAEURL] isEqual:url]) {
   206 				existingLoginItemIndex = [(NSArray *)loginItems indexOfObjectIdenticalTo:loginItemDict];
   207 				break;
   208 			}
   209 		}
   210 	}
   211 
   212 	if (enabled && (existingLoginItemIndex == -1))
   213 		LIAEAddURLAtEnd((CFURLRef)url, false);
   214 	else if (!enabled && (existingLoginItemIndex != -1))
   215 		LIAERemove(existingLoginItemIndex);
   216 
   217 	if(loginItems)
   218 		CFRelease(loginItems);
   219 }
   220 
   221 #pragma mark -
   222 #pragma mark GrowlMenu running state
   223 
   224 - (void) enableGrowlMenu {
   225 	NSBundle *bundle = [NSBundle bundleForClass:[GrowlPreferencesController class]];
   226 	NSString *growlMenuPath = [bundle pathForResource:@"GrowlMenu" ofType:@"app"];
   227 	NSURL *growlMenuURL = [NSURL fileURLWithPath:growlMenuPath];
   228 	[[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:growlMenuURL]
   229 	                withAppBundleIdentifier:nil
   230 	                                options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync
   231 	         additionalEventParamDescriptor:nil
   232 	                      launchIdentifiers:NULL];
   233 }
   234 
   235 - (void) disableGrowlMenu {
   236 	// Ask GrowlMenu to shutdown via the DNC
   237 	CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
   238 										 CFSTR("GrowlMenuShutdown"),
   239 										 /*object*/ NULL,
   240 										 /*userInfo*/ NULL,
   241 										 /*deliverImmediately*/ false);
   242 }
   243 
   244 #pragma mark -
   245 #pragma mark Growl running state
   246 
   247 - (void) setGrowlRunning:(BOOL)flag noMatterWhat:(BOOL)nmw {
   248 	// Store the desired running-state of the helper app for use by GHA.
   249 	[self setBool:flag forKey:GrowlEnabledKey];
   250 
   251 	//now launch or terminate as appropriate.
   252 	if (flag)
   253 		[self launchGrowl:nmw];
   254 	else
   255 		[self terminateGrowl];
   256 }
   257 
   258 - (BOOL) isRunning:(NSString *)theBundleIdentifier {
   259 	BOOL isRunning = NO;
   260 	ProcessSerialNumber PSN = { kNoProcess, kNoProcess };
   261 
   262 	while (GetNextProcess(&PSN) == noErr) {
   263 		NSDictionary *infoDict = (NSDictionary *)ProcessInformationCopyDictionary(&PSN, kProcessDictionaryIncludeAllInformationMask);
   264 		if(infoDict) {
   265 			NSString *bundleID = [infoDict objectForKey:(NSString *)kCFBundleIdentifierKey];
   266 			isRunning = bundleID && [bundleID isEqualToString:theBundleIdentifier];
   267 			CFMakeCollectable(infoDict);
   268 			[infoDict release];
   269 		}
   270 		if (isRunning)
   271 			break;
   272 	}
   273 
   274 	return isRunning;
   275 }
   276 
   277 - (BOOL) isGrowlRunning {
   278 	return [self isRunning:@"com.Growl.GrowlHelperApp"];
   279 }
   280 
   281 - (void) launchGrowl:(BOOL)noMatterWhat {
   282 	NSString *helperPath = [[GrowlPathUtilities helperAppBundle] bundlePath];
   283 	NSURL *helperURL = [NSURL fileURLWithPath:helperPath];
   284 
   285 	unsigned options = NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync;
   286 	if (noMatterWhat)
   287 		options |= NSWorkspaceLaunchNewInstance;
   288 	[[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:helperURL]
   289 	                withAppBundleIdentifier:nil
   290 	                                options:options
   291 	         additionalEventParamDescriptor:nil
   292 	                      launchIdentifiers:NULL];
   293 }
   294 
   295 - (void) terminateGrowl {
   296 	// Ask the Growl Helper App to shutdown via the DNC
   297 	CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
   298 										 (CFStringRef)GROWL_SHUTDOWN,
   299 										 /*object*/ NULL,
   300 										 /*userInfo*/ NULL,
   301 										 /*deliverImmediately*/ false);
   302 }
   303 
   304 #pragma mark -
   305 //Simplified accessors
   306 
   307 #pragma mark UI
   308 
   309 - (CFIndex)selectedPosition {
   310 	return [self integerForKey:GROWL_POSITION_PREFERENCE_KEY];
   311 }
   312 
   313 - (BOOL) isBackgroundUpdateCheckEnabled {
   314 	return [self boolForKey:GrowlUpdateCheckKey];
   315 }
   316 - (void) setIsBackgroundUpdateCheckEnabled:(BOOL)flag {
   317 	[self setBool:flag forKey:GrowlUpdateCheckKey];
   318 }
   319 
   320 - (NSString *) defaultDisplayPluginName {
   321 	return [self objectForKey:GrowlDisplayPluginKey];
   322 }
   323 - (void) setDefaultDisplayPluginName:(NSString *)name {
   324 	[self setObject:name forKey:GrowlDisplayPluginKey];
   325 }
   326 
   327 - (BOOL) squelchMode {
   328 	return [self boolForKey:GrowlSquelchModeKey];
   329 }
   330 - (void) setSquelchMode:(BOOL)flag {
   331 	[self setBool:flag forKey:GrowlSquelchModeKey];
   332 }
   333 
   334 - (BOOL) stickyWhenAway {
   335 	return [self boolForKey:GrowlStickyWhenAwayKey];
   336 }
   337 - (void) setStickyWhenAway:(BOOL)flag {
   338 	[self setBool:flag forKey:GrowlStickyWhenAwayKey];
   339 }
   340 
   341 - (NSNumber*) idleThreshold {
   342 #ifdef __LP64__
   343 	return [NSNumber numberWithInteger:[self integerForKey:GrowlStickyIdleThresholdKey]];
   344 #else
   345 	return [NSNumber numberWithInt:[self integerForKey:GrowlStickyIdleThresholdKey]];
   346 #endif
   347 }
   348 
   349 - (void) setIdleThreshold:(NSNumber*)value {
   350 	[self setInteger:[value intValue] forKey:GrowlStickyIdleThresholdKey];
   351 }
   352 #pragma mark Status Item
   353 
   354 - (BOOL) isGrowlMenuEnabled {
   355 	return [self boolForKey:GrowlMenuExtraKey];
   356 }
   357 
   358 - (void) setGrowlMenuEnabled:(BOOL)state {
   359 	if (state != [self isGrowlMenuEnabled]) {
   360 		[self setBool:state forKey:GrowlMenuExtraKey];
   361 		if (state)
   362 			[self enableGrowlMenu];
   363 		else
   364 			[self disableGrowlMenu];
   365 	}
   366 }
   367 
   368 #pragma mark Logging
   369 
   370 - (BOOL) loggingEnabled {
   371 	return [self boolForKey:GrowlLoggingEnabledKey];
   372 }
   373 
   374 - (void) setLoggingEnabled:(BOOL)flag {
   375 	[self setBool:flag forKey:GrowlLoggingEnabledKey];
   376 }
   377 
   378 - (BOOL) isGrowlServerEnabled {
   379 	return [self boolForKey:GrowlStartServerKey];
   380 }
   381 
   382 - (void) setGrowlServerEnabled:(BOOL)enabled {
   383 	[self setBool:enabled forKey:GrowlStartServerKey];
   384 }
   385 
   386 #pragma mark Remote Growling
   387 
   388 - (BOOL) isRemoteRegistrationAllowed {
   389 	return [self boolForKey:GrowlRemoteRegistrationKey];
   390 }
   391 
   392 - (void) setRemoteRegistrationAllowed:(BOOL)flag {
   393 	[self setBool:flag forKey:GrowlRemoteRegistrationKey];
   394 }
   395 
   396 - (NSString *) remotePassword {
   397 	unsigned char *password;
   398 	UInt32 passwordLength;
   399 	OSStatus status;
   400 	status = SecKeychainFindGenericPassword(NULL,
   401 											(UInt32)strlen(keychainServiceName), keychainServiceName,
   402 											(UInt32)strlen(keychainAccountName), keychainAccountName,
   403 											&passwordLength, (void **)&password, NULL);
   404 
   405 	NSString *passwordString;
   406 	if (status == noErr) {
   407 		passwordString = (NSString *)CFStringCreateWithBytes(kCFAllocatorDefault, password, passwordLength, kCFStringEncodingUTF8, false);
   408 		if(passwordString) {
   409 			CFMakeCollectable(passwordString);
   410 			[passwordString autorelease];
   411 			SecKeychainItemFreeContent(NULL, password);
   412 		}
   413 	} else {
   414 		if (status != errSecItemNotFound)
   415 			NSLog(@"Failed to retrieve password from keychain. Error: %d", status);
   416 		passwordString = @"";
   417 	}
   418 
   419 	return passwordString;
   420 }
   421 
   422 - (void) setRemotePassword:(NSString *)value {
   423 	const char *password = value ? [value UTF8String] : "";
   424 	size_t length = strlen(password);
   425 	OSStatus status;
   426 	SecKeychainItemRef itemRef = nil;
   427 	status = SecKeychainFindGenericPassword(NULL,
   428 											(UInt32)strlen(keychainServiceName), keychainServiceName,
   429 											(UInt32)strlen(keychainAccountName), keychainAccountName,
   430 											NULL, NULL, &itemRef);
   431 	if (status == errSecItemNotFound) {
   432 		// add new item
   433 		status = SecKeychainAddGenericPassword(NULL,
   434 											   (UInt32)strlen(keychainServiceName), keychainServiceName,
   435 											   (UInt32)strlen(keychainAccountName), keychainAccountName,
   436 											   (UInt32)length, password, NULL);
   437 		if (status)
   438 			NSLog(@"Failed to add password to keychain.");
   439 	} else {
   440 		// change existing password
   441 		SecKeychainAttribute attrs[] = {
   442 			{ kSecAccountItemAttr, (UInt32)strlen(keychainAccountName), (char *)keychainAccountName },
   443 			{ kSecServiceItemAttr, (UInt32)strlen(keychainServiceName), (char *)keychainServiceName }
   444 		};
   445 		const SecKeychainAttributeList attributes = { (UInt32)sizeof(attrs) / (UInt32)sizeof(attrs[0]), attrs };
   446 		status = SecKeychainItemModifyAttributesAndData(itemRef,		// the item reference
   447 														&attributes,	// no change to attributes
   448 														(UInt32)length,			// length of password
   449 														password		// pointer to password data
   450 														);
   451 		if (itemRef)
   452 			CFRelease(itemRef);
   453 		if (status)
   454 			NSLog(@"Failed to change password in keychain.");
   455 	}
   456 }
   457 
   458 - (unsigned short) UDPPort {
   459 	return [self unsignedShortForKey:GrowlUDPPortKey];
   460 }
   461 - (void) setUDPPort:(unsigned short)value {
   462 	[self setUnsignedShort:value forKey:GrowlUDPPortKey];
   463 }
   464 
   465 - (BOOL) isForwardingEnabled {
   466 	return [self boolForKey:GrowlEnableForwardKey];
   467 }
   468 - (void) setForwardingEnabled:(BOOL)enabled {
   469 	[self setBool:enabled forKey:GrowlEnableForwardKey];
   470 }
   471 
   472 #pragma mark -
   473 /*
   474  * @brief Growl preferences changed
   475  *
   476  * Synchronize our NSUserDefaults to immediately get any changes from the disk
   477  */
   478 - (void) growlPreferencesChanged:(NSNotification *)notification {
   479 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   480 
   481 	NSString *object = [notification object];
   482 //	NSLog(@"%s: %@\n", __func__, object);
   483 	SYNCHRONIZE_GROWL_PREFS();
   484 	if (!object || [object isEqualToString:GrowlDisplayPluginKey]) {
   485 		[self willChangeValueForKey:@"defaultDisplayPluginName"];
   486 		[self didChangeValueForKey:@"defaultDisplayPluginName"];
   487 	}
   488 	if (!object || [object isEqualToString:GrowlSquelchModeKey]) {
   489 		[self willChangeValueForKey:@"squelchMode"];
   490 		[self didChangeValueForKey:@"squelchMode"];
   491 	}
   492 	if (!object || [object isEqualToString:GrowlMenuExtraKey]) {
   493 		[self willChangeValueForKey:@"growlMenuEnabled"];
   494 		[self didChangeValueForKey:@"growlMenuEnabled"];
   495 	}
   496 	if (!object || [object isEqualToString:GrowlEnableForwardKey]) {
   497 		[self willChangeValueForKey:@"forwardingEnabled"];
   498 		[self didChangeValueForKey:@"forwardingEnabled"];
   499 	}
   500 	if (!object || [object isEqualToString:GrowlUpdateCheckKey]) {
   501 		[self willChangeValueForKey:@"backgroundUpdateCheckEnabled"];
   502 		[self didChangeValueForKey:@"backgroundUpdateCheckEnabled"];
   503 	}
   504 	if (!object || [object isEqualToString:GrowlStickyWhenAwayKey]) {
   505 		[self willChangeValueForKey:@"stickyWhenAway"];
   506 		[self didChangeValueForKey:@"stickyWhenAway"];
   507 	}
   508 	if (!object || [object isEqualToString:GrowlStickyIdleThresholdKey]) {
   509 		[self willChangeValueForKey:@"idleThreshold"];
   510 		[self didChangeValueForKey:@"idleThreshold"];
   511 	}
   512 	if (!object || [object isEqualToString:GrowlRemoteRegistrationKey]) {
   513 		[self willChangeValueForKey:@"remoteRegistrationAllowed"];
   514 		[self didChangeValueForKey:@"remoteRegistrationAllowed"];
   515 	}
   516 	
   517 	[pool release];
   518 }
   519 
   520 @end