if the user has a corrupt install and as a result pathForResource:ofType: returns nil for GHA, CFURLCreateWithFileSystemPath will crash, so we guard the call and the rest of the block since it depends on that path and log a message to console indicating that the user will need to reinstall Growl, as well as the returned value of prefPaneBundle and pathToGHA.
2 // GrowlPreferencesController.m
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.
9 // This file is under the BSD License, refer to License.txt for details
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 <Security/SecKeychain.h>
20 #include <Security/SecKeychainItem.h>
22 #define keychainServiceName "Growl"
23 #define keychainAccountName "Growl"
25 CFTypeRef GrowlPreferencesController_objectForKey(CFTypeRef key) {
26 return [[GrowlPreferencesController sharedController] objectForKey:(id)key];
29 CFIndex GrowlPreferencesController_integerForKey(CFTypeRef key) {
30 Boolean keyExistsAndHasValidFormat;
31 return CFPreferencesGetAppIntegerValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
34 Boolean GrowlPreferencesController_boolForKey(CFTypeRef key) {
35 Boolean keyExistsAndHasValidFormat;
36 return CFPreferencesGetAppBooleanValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
39 unsigned short GrowlPreferencesController_unsignedShortForKey(CFTypeRef key)
41 CFIndex theIndex = GrowlPreferencesController_integerForKey(key);
43 if (theIndex > USHRT_MAX)
45 else if (theIndex < 0)
47 return (unsigned short)theIndex;
50 @implementation GrowlPreferencesController
52 + (GrowlPreferencesController *) sharedController {
53 return [self sharedInstance];
56 - (id) initSingleton {
57 if ((self = [super initSingleton])) {
58 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
59 selector:@selector(growlPreferencesChanged:)
60 name:GrowlPreferencesChanged
62 loginItems = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, /*options*/ NULL);
68 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
69 CFRelease(loginItems);
76 - (void) registerDefaults:(NSDictionary *)inDefaults {
77 NSUserDefaults *helperAppDefaults = [[NSUserDefaults alloc] init];
78 [helperAppDefaults addSuiteNamed:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
79 NSDictionary *existing = [helperAppDefaults persistentDomainForName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
81 NSMutableDictionary *domain = [inDefaults mutableCopy];
82 [domain addEntriesFromDictionary:existing];
83 [helperAppDefaults setPersistentDomain:domain forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
86 [helperAppDefaults setPersistentDomain:inDefaults forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
88 [helperAppDefaults release];
89 SYNCHRONIZE_GROWL_PREFS();
92 - (id) objectForKey:(NSString *)key {
93 id value = (id)CFPreferencesCopyAppValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
95 CFMakeCollectable(value);
96 return [value autorelease];
99 - (void) setObject:(id)object forKey:(NSString *)key {
100 CFPreferencesSetAppValue((CFStringRef)key,
101 (CFPropertyListRef)object,
102 (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
104 SYNCHRONIZE_GROWL_PREFS();
107 CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
108 CFStringRef pidKey = CFSTR("pid");
109 CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&pidKey, (const void **)&pidValue, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
111 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
112 (CFStringRef)GrowlPreferencesChanged,
114 /*userInfo*/ userInfo,
115 /*deliverImmediately*/ false);
119 - (BOOL) boolForKey:(NSString *)key {
120 return GrowlPreferencesController_boolForKey((CFTypeRef)key);
123 - (void) setBool:(BOOL)value forKey:(NSString *)key {
124 NSNumber *object = [[NSNumber alloc] initWithBool:value];
125 [self setObject:object forKey:key];
129 - (CFIndex) integerForKey:(NSString *)key {
130 return GrowlPreferencesController_integerForKey((CFTypeRef)key);
133 - (void) setInteger:(CFIndex)value forKey:(NSString *)key {
135 NSNumber *object = [[NSNumber alloc] initWithInteger:value];
137 NSNumber *object = [[NSNumber alloc] initWithInt:value];
139 [self setObject:object forKey:key];
143 - (unsigned short)unsignedShortForKey:(NSString *)key
145 return GrowlPreferencesController_unsignedShortForKey((CFTypeRef)key);
149 - (void)setUnsignedShort:(unsigned short)theShort forKey:(NSString *)key
151 [self setObject:[NSNumber numberWithUnsignedShort:theShort] forKey:key];
154 - (void) synchronize {
155 SYNCHRONIZE_GROWL_PREFS();
159 #pragma mark Start-at-login control
161 - (BOOL) shouldStartGrowlAtLogin {
162 Boolean foundIt = false;
164 //get the prefpane bundle and find GHA within it.
165 NSBundle *prefPaneBundle = [NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER];
166 NSString *pathToGHA = [prefPaneBundle pathForResource:@"GrowlHelperApp" ofType:@"app"];
168 //get the file url to GHA.
169 CFURLRef urlToGHA = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)pathToGHA, kCFURLPOSIXPathStyle, true);
172 NSArray *currentLoginItems = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, &seed)) autorelease];
173 for (id itemObject in currentLoginItems) {
174 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
176 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
178 OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL);
180 foundIt = CFEqual(URL, urlToGHA);
191 NSLog(@"Growl: your install is corrupt, you will need to reinstall\nyour prefpane bundle is:%@\n your pathToGHA is:%@", prefPaneBundle, pathToGHA);
197 - (void) setShouldStartGrowlAtLogin:(BOOL)flag {
198 //get the prefpane bundle and find GHA within it.
199 NSString *pathToGHA = [[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] pathForResource:@"GrowlHelperApp" ofType:@"app"];
200 [self setStartAtLogin:pathToGHA enabled:flag];
203 - (void) setStartAtLogin:(NSString *)path enabled:(BOOL)enabled {
205 CFURLRef URLToToggle = (CFURLRef)[NSURL fileURLWithPath:path];
206 LSSharedFileListItemRef existingItem = NULL;
209 NSArray *currentLoginItems = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, &seed)) autorelease];
210 for (id itemObject in currentLoginItems) {
211 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
213 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
215 OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL);
217 Boolean foundIt = CFEqual(URL, URLToToggle);
227 if (enabled && (existingItem == NULL)) {
228 NSString *displayName = [[NSFileManager defaultManager] displayNameAtPath:path];
231 Boolean gotRef = CFURLGetFSRef(URLToToggle, &ref);
233 status = GetIconRefFromFileInfo(&ref,
234 /*fileNameLength*/ 0, /*fileName*/ NULL,
235 kFSCatInfoNone, /*catalogInfo*/ NULL,
236 kIconServicesNormalUsageFlag,
243 LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, (CFStringRef)displayName, icon, URLToToggle, /*propertiesToSet*/ NULL, /*propertiesToClear*/ NULL);
244 } else if (!enabled && (existingItem != NULL))
245 LSSharedFileListItemRemove(loginItems, existingItem);
249 #pragma mark GrowlMenu running state
251 - (void) enableGrowlMenu {
252 NSBundle *bundle = [NSBundle bundleForClass:[GrowlPreferencesController class]];
253 NSString *growlMenuPath = [bundle pathForResource:@"GrowlMenu" ofType:@"app"];
254 NSURL *growlMenuURL = [NSURL fileURLWithPath:growlMenuPath];
255 [[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:growlMenuURL]
256 withAppBundleIdentifier:nil
257 options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync
258 additionalEventParamDescriptor:nil
259 launchIdentifiers:NULL];
262 - (void) disableGrowlMenu {
263 // Ask GrowlMenu to shutdown via the DNC
264 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
265 CFSTR("GrowlMenuShutdown"),
268 /*deliverImmediately*/ false);
272 #pragma mark Growl running state
274 - (void) setGrowlRunning:(BOOL)flag noMatterWhat:(BOOL)nmw {
275 // Store the desired running-state of the helper app for use by GHA.
276 [self setBool:flag forKey:GrowlEnabledKey];
278 //now launch or terminate as appropriate.
280 [self launchGrowl:nmw];
282 [self terminateGrowl];
285 - (BOOL) isRunning:(NSString *)theBundleIdentifier {
287 ProcessSerialNumber PSN = { kNoProcess, kNoProcess };
289 while (GetNextProcess(&PSN) == noErr) {
290 NSDictionary *infoDict = (NSDictionary *)ProcessInformationCopyDictionary(&PSN, kProcessDictionaryIncludeAllInformationMask);
292 NSString *bundleID = [infoDict objectForKey:(NSString *)kCFBundleIdentifierKey];
293 isRunning = bundleID && [bundleID isEqualToString:theBundleIdentifier];
294 CFMakeCollectable(infoDict);
304 - (BOOL) isGrowlRunning {
305 return [self isRunning:@"com.Growl.GrowlHelperApp"];
308 - (void) launchGrowl:(BOOL)noMatterWhat {
309 NSString *helperPath = [[GrowlPathUtilities helperAppBundle] bundlePath];
310 NSURL *helperURL = [NSURL fileURLWithPath:helperPath];
312 unsigned options = NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync;
314 options |= NSWorkspaceLaunchNewInstance;
315 [[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:helperURL]
316 withAppBundleIdentifier:nil
318 additionalEventParamDescriptor:nil
319 launchIdentifiers:NULL];
322 - (void) terminateGrowl {
323 // Ask the Growl Helper App to shutdown via the DNC
324 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
325 (CFStringRef)GROWL_SHUTDOWN,
328 /*deliverImmediately*/ false);
332 //Simplified accessors
336 - (CFIndex)selectedPosition {
337 return [self integerForKey:GROWL_POSITION_PREFERENCE_KEY];
340 - (BOOL) isBackgroundUpdateCheckEnabled {
341 return [self boolForKey:GrowlUpdateCheckKey];
343 - (void) setIsBackgroundUpdateCheckEnabled:(BOOL)flag {
344 [self setBool:flag forKey:GrowlUpdateCheckKey];
347 - (NSString *) defaultDisplayPluginName {
348 return [self objectForKey:GrowlDisplayPluginKey];
350 - (void) setDefaultDisplayPluginName:(NSString *)name {
351 [self setObject:name forKey:GrowlDisplayPluginKey];
354 - (BOOL) squelchMode {
355 return [self boolForKey:GrowlSquelchModeKey];
357 - (void) setSquelchMode:(BOOL)flag {
358 [self setBool:flag forKey:GrowlSquelchModeKey];
361 - (BOOL) stickyWhenAway {
362 return [self boolForKey:GrowlStickyWhenAwayKey];
364 - (void) setStickyWhenAway:(BOOL)flag {
365 [self setBool:flag forKey:GrowlStickyWhenAwayKey];
368 - (NSNumber*) idleThreshold {
370 return [NSNumber numberWithInteger:[self integerForKey:GrowlStickyIdleThresholdKey]];
372 return [NSNumber numberWithInt:[self integerForKey:GrowlStickyIdleThresholdKey]];
376 - (void) setIdleThreshold:(NSNumber*)value {
377 [self setInteger:[value intValue] forKey:GrowlStickyIdleThresholdKey];
379 #pragma mark Status Item
381 - (BOOL) isGrowlMenuEnabled {
382 return [self boolForKey:GrowlMenuExtraKey];
385 - (void) setGrowlMenuEnabled:(BOOL)state {
386 if (state != [self isGrowlMenuEnabled]) {
387 [self setBool:state forKey:GrowlMenuExtraKey];
389 [self enableGrowlMenu];
391 [self disableGrowlMenu];
397 - (BOOL) loggingEnabled {
398 return [self boolForKey:GrowlLoggingEnabledKey];
401 - (void) setLoggingEnabled:(BOOL)flag {
402 [self setBool:flag forKey:GrowlLoggingEnabledKey];
405 - (BOOL) isGrowlServerEnabled {
406 return [self boolForKey:GrowlStartServerKey];
409 - (void) setGrowlServerEnabled:(BOOL)enabled {
410 [self setBool:enabled forKey:GrowlStartServerKey];
413 #pragma mark Remote Growling
415 - (BOOL) isRemoteRegistrationAllowed {
416 return [self boolForKey:GrowlRemoteRegistrationKey];
419 - (void) setRemoteRegistrationAllowed:(BOOL)flag {
420 [self setBool:flag forKey:GrowlRemoteRegistrationKey];
423 - (NSString *) remotePassword {
424 unsigned char *password;
425 UInt32 passwordLength;
427 status = SecKeychainFindGenericPassword(NULL,
428 (UInt32)strlen(keychainServiceName), keychainServiceName,
429 (UInt32)strlen(keychainAccountName), keychainAccountName,
430 &passwordLength, (void **)&password, NULL);
432 NSString *passwordString;
433 if (status == noErr) {
434 passwordString = (NSString *)CFStringCreateWithBytes(kCFAllocatorDefault, password, passwordLength, kCFStringEncodingUTF8, false);
436 CFMakeCollectable(passwordString);
437 [passwordString autorelease];
438 SecKeychainItemFreeContent(NULL, password);
441 if (status != errSecItemNotFound)
442 NSLog(@"Failed to retrieve password from keychain. Error: %d", status);
443 passwordString = @"";
446 return passwordString;
449 - (void) setRemotePassword:(NSString *)value {
450 const char *password = value ? [value UTF8String] : "";
451 size_t length = strlen(password);
453 SecKeychainItemRef itemRef = nil;
454 status = SecKeychainFindGenericPassword(NULL,
455 (UInt32)strlen(keychainServiceName), keychainServiceName,
456 (UInt32)strlen(keychainAccountName), keychainAccountName,
457 NULL, NULL, &itemRef);
458 if (status == errSecItemNotFound) {
460 status = SecKeychainAddGenericPassword(NULL,
461 (UInt32)strlen(keychainServiceName), keychainServiceName,
462 (UInt32)strlen(keychainAccountName), keychainAccountName,
463 (UInt32)length, password, NULL);
465 NSLog(@"Failed to add password to keychain.");
467 // change existing password
468 SecKeychainAttribute attrs[] = {
469 { kSecAccountItemAttr, (UInt32)strlen(keychainAccountName), (char *)keychainAccountName },
470 { kSecServiceItemAttr, (UInt32)strlen(keychainServiceName), (char *)keychainServiceName }
472 const SecKeychainAttributeList attributes = { (UInt32)sizeof(attrs) / (UInt32)sizeof(attrs[0]), attrs };
473 status = SecKeychainItemModifyAttributesAndData(itemRef, // the item reference
474 &attributes, // no change to attributes
475 (UInt32)length, // length of password
476 password // pointer to password data
481 NSLog(@"Failed to change password in keychain.");
485 - (unsigned short) UDPPort {
486 return [self unsignedShortForKey:GrowlUDPPortKey];
488 - (void) setUDPPort:(unsigned short)value {
489 [self setUnsignedShort:value forKey:GrowlUDPPortKey];
492 - (BOOL) isForwardingEnabled {
493 return [self boolForKey:GrowlEnableForwardKey];
495 - (void) setForwardingEnabled:(BOOL)enabled {
496 [self setBool:enabled forKey:GrowlEnableForwardKey];
501 * @brief Growl preferences changed
503 * Synchronize our NSUserDefaults to immediately get any changes from the disk
505 - (void) growlPreferencesChanged:(NSNotification *)notification {
506 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
508 NSString *object = [notification object];
509 // NSLog(@"%s: %@\n", __func__, object);
510 SYNCHRONIZE_GROWL_PREFS();
511 if (!object || [object isEqualToString:GrowlDisplayPluginKey]) {
512 [self willChangeValueForKey:@"defaultDisplayPluginName"];
513 [self didChangeValueForKey:@"defaultDisplayPluginName"];
515 if (!object || [object isEqualToString:GrowlSquelchModeKey]) {
516 [self willChangeValueForKey:@"squelchMode"];
517 [self didChangeValueForKey:@"squelchMode"];
519 if (!object || [object isEqualToString:GrowlMenuExtraKey]) {
520 [self willChangeValueForKey:@"growlMenuEnabled"];
521 [self didChangeValueForKey:@"growlMenuEnabled"];
523 if (!object || [object isEqualToString:GrowlEnableForwardKey]) {
524 [self willChangeValueForKey:@"forwardingEnabled"];
525 [self didChangeValueForKey:@"forwardingEnabled"];
527 if (!object || [object isEqualToString:GrowlUpdateCheckKey]) {
528 [self willChangeValueForKey:@"backgroundUpdateCheckEnabled"];
529 [self didChangeValueForKey:@"backgroundUpdateCheckEnabled"];
531 if (!object || [object isEqualToString:GrowlStickyWhenAwayKey]) {
532 [self willChangeValueForKey:@"stickyWhenAway"];
533 [self didChangeValueForKey:@"stickyWhenAway"];
535 if (!object || [object isEqualToString:GrowlStickyIdleThresholdKey]) {
536 [self willChangeValueForKey:@"idleThreshold"];
537 [self didChangeValueForKey:@"idleThreshold"];
539 if (!object || [object isEqualToString:GrowlRemoteRegistrationKey]) {
540 [self willChangeValueForKey:@"remoteRegistrationAllowed"];
541 [self didChangeValueForKey:@"remoteRegistrationAllowed"];