Core/Source/GrowlPreferencesController.m
author Peter Hosey <hg@boredzo.org>
Tue Jul 14 06:30:22 2009 -0700 (2009-07-14)
changeset 4240 c85bf1983721
parent 4016 3b1da316d31a
child 4246 4f52d1d98978
permissions -rw-r--r--
Fix GrowlSafari for Safari 4.0.x on Tiger.

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