Extras/GrowlSafari/GrowlSafari.m
author Peter Hosey <hg@boredzo.org>
Thu Jul 02 06:30:46 2009 -0700 (2009-07-02)
changeset 4225 22ffbf7ae378
parent 4077 a571d8dd2fb0
child 4226 a6eb28fcc0b7
permissions -rw-r--r--
Fix an apparent leak: We put the download-started date into our dictionary under the download's identifier, but then tried to remove the date from the dictionary under the download itself. Thus, we leaked both the date and the copy of the identifier.
     1 /*
     2  Copyright (c) The Growl Project, 2004-2005
     3  All rights reserved.
     4 
     5 
     6  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
     7 
     8 
     9  1. Redistributions of source code must retain the above copyright
    10  notice, this list of conditions and the following disclaimer.
    11  2. Redistributions in binary form must reproduce the above copyright
    12  notice, this list of conditions and the following disclaimer in the
    13  documentation and/or other materials provided with the distribution.
    14  3. Neither the name of Growl nor the names of its contributors
    15  may be used to endorse or promote products derived from this software
    16  without specific prior written permission.
    17 
    18 
    19  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    20 
    21  */
    22 
    23 //
    24 //  GrowlSafari.m
    25 //  GrowlSafari
    26 //
    27 //  Created by Kevin Ballard on 10/29/04.
    28 //  Copyright 2004 Kevin Ballard. All rights reserved.
    29 //
    30 
    31 #import "GrowlSafari.h"
    32 #import "GSWebBookmark.h"
    33 #import <Growl/Growl.h>
    34 #import <objc/objc-runtime.h>
    35 
    36 
    37 #define SAFARI_VERSION_2_0	412
    38 #define SAFARI_VERSION_3_0  523
    39 
    40 // How long should we wait (in seconds) before it's a long download?
    41 static double longDownload = 15.0;
    42 static int safariVersion;
    43 static NSMutableDictionary *dates = nil;
    44 
    45 // Using method swizzling as outlined here:
    46 // http://www.cocoadev.com/index.pl?MethodSwizzling
    47 // A couple of modifications made to support swizzling class methods
    48 
    49 static BOOL PerformSwizzle(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
    50     // First, make sure the class isn't nil
    51 	if (aClass) {
    52 		Method orig_method = nil, alt_method = nil;
    53 
    54 		// Next, look for the methods
    55 		if (forInstance) {
    56 			orig_method = class_getInstanceMethod(aClass, orig_sel);
    57 			alt_method = class_getInstanceMethod(aClass, alt_sel);
    58 		} else {
    59 			orig_method = class_getClassMethod(aClass, orig_sel);
    60 			alt_method = class_getClassMethod(aClass, alt_sel);
    61 		}
    62 
    63 		// If both are found, swizzle them
    64 		if (orig_method && alt_method) {
    65 			IMP temp;
    66 
    67 			temp = orig_method->method_imp;
    68 			orig_method->method_imp = alt_method->method_imp;
    69 			alt_method->method_imp = temp;
    70 
    71 			return YES;
    72 		} else {
    73 			// This bit stolen from SubEthaFari's source
    74 			NSLog(@"GrowlSafari Error: Original (selector %s) %@, Alternate (selector %s) %@",
    75 				  orig_sel,
    76 				  orig_method ? @"was found" : @"not found",
    77 				  alt_sel,
    78 				  alt_method ? @"was found" : @"not found");
    79 		}
    80 	} else {
    81 		NSLog(@"%@", @"GrowlSafari Error: No class to swizzle methods in");
    82 	}
    83 
    84 	return NO;
    85 }
    86 
    87 static void setDownloadStarted(id dl) {
    88 	if (!dates)
    89 		dates = [[NSMutableDictionary alloc] init];
    90 
    91 	[dates setObject:[NSDate date] forKey:[dl identifier]];
    92 }
    93 
    94 static NSDate *dateStarted(id dl) {
    95 	if (dates)
    96 		return [dates objectForKey:[dl identifier]];
    97 
    98 	return nil;
    99 }
   100 
   101 static BOOL isLongDownload(id dl) {
   102 	NSDate *date = dateStarted(dl);
   103 	return (date && -[date timeIntervalSinceNow] > longDownload);
   104 }
   105 
   106 static void setDownloadFinished(id dl) {
   107 	[dates removeObjectForKey:[dl identifier]];
   108 }
   109 
   110 @implementation GrowlSafari
   111 + (NSBundle *) bundle {
   112 	return [NSBundle bundleWithIdentifier:@"com.growl.GrowlSafari"];
   113 }
   114 
   115 + (NSString *) bundleVersion {
   116 	return [[[GrowlSafari bundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
   117 }
   118 
   119 + (void) initialize {
   120 	NSString *growlPath = [[[GrowlSafari bundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
   121 	NSBundle *growlBundle = [NSBundle bundleWithPath:growlPath];
   122 
   123 	if (growlBundle && [growlBundle load]) {
   124 		// Register ourselves as a Growl delegate
   125 		[GrowlApplicationBridge setGrowlDelegate:self];
   126 
   127 		safariVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey] intValue];
   128 		//  NSLog(@"%d",safariVersion);
   129 
   130 		//	NSLog(@"Patching DownloadProgressEntry...");
   131 		Class class = NSClassFromString(@"DownloadProgressEntry");
   132 		PerformSwizzle(class, @selector(setDownloadStage:), @selector(mySetDownloadStage:), YES);
   133 				
   134 		if (safariVersion<SAFARI_VERSION_3_0) 
   135 			PerformSwizzle(class, @selector(updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
   136 		else
   137 			PerformSwizzle(class, @selector(_updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
   138 		
   139 		PerformSwizzle(class, @selector(initWithDownload:mayOpenWhenDone:allowOverwrite:),
   140 					   @selector(myInitWithDownload:mayOpenWhenDone:allowOverwrite:),
   141 					   YES);
   142 		
   143 		Class webBookmarkClass = NSClassFromString(@"WebBookmark");
   144 		if (webBookmarkClass)
   145 			[[GSWebBookmark class] poseAsClass:webBookmarkClass];
   146 
   147 		NSLog(@"Loaded GrowlSafari %@", [GrowlSafari bundleVersion]);
   148 		NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
   149 		NSLog(@"Using Growl.framework %@ (%@)",
   150 			  [infoDictionary objectForKey:@"CFBundleShortVersionString"],
   151 			  [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
   152 	} else {
   153 		NSLog(@"Could not load Growl.framework, GrowlSafari disabled");
   154 	}
   155 	
   156 }
   157 
   158 #pragma mark GrowlApplicationBridge delegate methods
   159 
   160 + (NSString *) applicationNameForGrowl {
   161 	return @"GrowlSafari";
   162 }
   163 
   164 + (NSImage *) applicationIconForGrowl {
   165 	return [NSImage imageNamed:@"NSApplicationIcon"];
   166 }
   167 
   168 + (NSDictionary *) registrationDictionaryForGrowl {
   169 	NSBundle *bundle = [GrowlSafari bundle];
   170 	NSArray *array = [[NSArray alloc] initWithObjects:
   171 		NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @""),
   172 		NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @""),
   173 		NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @""),
   174 		NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @""),
   175 		NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @""),
   176 		nil];
   177 	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
   178 		array, GROWL_NOTIFICATIONS_DEFAULT,
   179 		array, GROWL_NOTIFICATIONS_ALL,
   180 		nil];
   181 	[array release];
   182 
   183 	return dict;
   184 }
   185 
   186 + (void) growlNotificationWasClicked:(id)clickContext {
   187 	NSURL *url = [[NSURL alloc] initWithString:clickContext];
   188 	[[NSWorkspace sharedWorkspace] openURL:url];
   189 	[url release];
   190 }
   191 
   192 + (void) notifyRSSUpdate:(WebBookmark *)bookmark newEntries:(int)newEntries {
   193 	NSBundle *bundle = [GrowlSafari bundle];
   194 	NSMutableString	*description = [[NSMutableString alloc]
   195 		initWithFormat:newEntries == 1 ? NSLocalizedStringFromTableInBundle(@"%d new entry", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"%d new entries", nil, bundle, @""),
   196 		newEntries,
   197 		[bookmark unreadRSSCount]];
   198 	if (newEntries != [bookmark unreadRSSCount])
   199 		[description appendFormat:NSLocalizedStringFromTableInBundle(@" (%d unread)", nil, bundle, @""), [bookmark unreadRSSCount]];
   200 
   201 	NSString *title = [bookmark title];
   202 	[GrowlApplicationBridge notifyWithTitle:(title ? title : [bookmark URLString])
   203 								description:description
   204 						   notificationName:NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @"")
   205 								   iconData:nil
   206 								   priority:0
   207 								   isSticky:NO
   208 							   clickContext:[bookmark URLString]];
   209 	[description release];
   210 }
   211 @end
   212 
   213 @implementation NSObject (GrowlSafariPatch)
   214 - (void) mySetDownloadStage:(int)stage {
   215 	int oldStage = [self downloadStage];
   216 	
   217 	//NSLog(@"mySetDownloadStage:%d -> %d", oldStage, stage);
   218 	[self mySetDownloadStage:stage];
   219 	if (dateStarted(self)) {
   220 		if ( (safariVersion < SAFARI_VERSION_3_0 && stage == 2) || (safariVersion >= SAFARI_VERSION_3_0 && stage == 1) ) {
   221 			NSBundle *bundle = [GrowlSafari bundle];
   222 			NSString *description = [[NSString alloc] initWithFormat:
   223 				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   224 				[[self gsDownloadPath] lastPathComponent]];
   225 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Decompressing File", nil, bundle, @"")
   226 										description:description
   227 								   notificationName:NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @"")
   228 										   iconData:nil
   229 										   priority:0
   230 										   isSticky:NO
   231 									   clickContext:nil];
   232 			[description release];
   233 		} else if ( (safariVersion < SAFARI_VERSION_3_0 && stage == 9 && oldStage != 9) || 
   234 			        (safariVersion >= SAFARI_VERSION_3_0 && stage == 8 && oldStage != 8) ) {
   235 			NSBundle *bundle = [GrowlSafari bundle];
   236 			NSString *description = [[NSString alloc] initWithFormat:
   237 				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   238 				[[self gsDownloadPath] lastPathComponent]];
   239 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Copying Disk Image", nil, bundle, @"")
   240 										description:description
   241 								   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
   242 										   iconData:nil
   243 										   priority:0
   244 										   isSticky:NO
   245 									   clickContext:nil];
   246 			[description release];
   247 		} else if ( (safariVersion < SAFARI_VERSION_2_0 && stage == 13) || 
   248 		            ((safariVersion >= SAFARI_VERSION_2_0 && safariVersion < SAFARI_VERSION_3_0) && stage == 15) ||
   249 				    (safariVersion >= SAFARI_VERSION_3_0 && stage == 13) ) {
   250 			NSBundle *bundle = [GrowlSafari bundle];
   251 			NSString *notificationName = isLongDownload(self) ? NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @"");
   252 			setDownloadFinished(self);
   253 			NSString *description = [[NSString alloc] initWithFormat:
   254 				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, "Message shown when a download is complete, where %@ becomes the filename"),
   255 				[self filename]];
   256 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"")
   257 										description:description
   258 								   notificationName:notificationName
   259 										   iconData:nil
   260 										   priority:0
   261 										   isSticky:NO
   262 									   clickContext:nil];
   263 			[description release];
   264 		}
   265 	} else if (stage == 0) {
   266 		setDownloadStarted(self);
   267 	}
   268 }
   269 
   270 - (void) myUpdateDiskImageStatus:(NSDictionary *)status {
   271 	int oldStage = [self downloadStage];
   272 	[self myUpdateDiskImageStatus:status];
   273 	//NSLog(@"myUpdateDiskImageStatus:%@ stage=%d -> %d", status, oldStage, [self downloadStage]);
   274 
   275 	if (dateStarted(self)
   276 			&& ( (safariVersion < SAFARI_VERSION_3_0 && oldStage == 3) || (safariVersion >= SAFARI_VERSION_3_0 && oldStage == 7) )
   277 			&& [self downloadStage] == 8
   278 			&& [[status objectForKey:@"status-stage"] isEqualToString:@"attach"]) {
   279 		NSBundle *bundle = [GrowlSafari bundle];
   280 		NSString *description = [[NSString alloc] initWithFormat:
   281 			NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   282 			[[self gsDownloadPath] lastPathComponent]];
   283 		[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Mounting Disk Image", nil, bundle, @"")
   284 									description:description
   285 							   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
   286 									   iconData:nil
   287 									   priority:0
   288 									   isSticky:NO
   289 								   clickContext:nil];
   290 		[description release];
   291 	}
   292 }
   293 
   294 // This is to make sure we're done with the pre-saved downloads
   295 - (id) myInitWithDownload:(id)fp8 mayOpenWhenDone:(BOOL)fp12 allowOverwrite:(BOOL)fp16 {
   296 	id retval = [self myInitWithDownload:fp8 mayOpenWhenDone:fp12 allowOverwrite:fp16];
   297 	setDownloadStarted(self);
   298 	return retval;
   299 }
   300 
   301 - (NSString*) gsDownloadPath {
   302 	if (safariVersion<SAFARI_VERSION_3_0)
   303 		return [self downloadPath];
   304 	else
   305 		return [self currentPath];
   306 }
   307 
   308 @end