~tcurdt/sparkle/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
//
//  SUUpdater.m
//  Sparkle
//
//  Created by Andy Matuschak on 1/4/06.
//  Copyright 2006 Andy Matuschak. All rights reserved.
//

#import "Sparkle.h"
#import "SUUpdater.h"

#import <uuid/uuid.h>

@interface SUUpdater (Private)
- (NSArray *)feedParameters;
- (BOOL)automaticallyUpdates;
- (BOOL)shouldScheduleUpdateCheck;
- (void)scheduleNextUpdateCheck;
- (NSTimeInterval)checkInterval;
- (NSURL *)feedURL;
@end

@implementation SUUpdater

#pragma mark Initialization

static SUUpdater *sharedUpdater = nil;

// SUUpdater's a singleton now! And I'm enforcing it!
// This will probably break the world if you try to write a Sparkle-enabled plugin for a Sparkle-enabled app.
+ (SUUpdater *)sharedUpdater
{
	if (sharedUpdater == nil)
		sharedUpdater = [[[self class] alloc] init];
	return sharedUpdater;
}

- (id)init
{
	self = [super init];
	if (sharedUpdater)
	{
		[self release];
		self = sharedUpdater;
	}
	else if (self != nil)
	{
		sharedUpdater = self;
		[self setHostBundle:[NSBundle mainBundle]];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:NSApplicationDidFinishLaunchingNotification object:NSApp];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(driverDidFinish:) name:SUUpdateDriverFinishedNotification object:nil];
		[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:[@"values." stringByAppendingString:SUScheduledCheckIntervalKey] options:0 context:NULL];
		[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:[@"values." stringByAppendingString:SUEnableAutomaticChecksKey] options:0 context:NULL];
	}
	return self;
}

- (void)applicationDidFinishLaunching:(NSNotification *)note
{
	// If the user has been asked about automatic checks and said no, get out of here.
	if ([[SUUserDefaults standardUserDefaults] objectForKey:SUEnableAutomaticChecksKey] &&
		[[SUUserDefaults standardUserDefaults] boolForKey:SUEnableAutomaticChecksKey] == NO) { return; }
	
	// Does the delegate want to take care of the logic for when we should ask permission to update?
	if ([delegate respondsToSelector:@selector(shouldPromptForPermissionToCheckForUpdatesToHostBundle:)])
	{
		if ([delegate shouldPromptForPermissionToCheckForUpdatesToHostBundle:hostBundle])
			[SUUpdatePermissionPrompt promptWithHostBundle:hostBundle delegate:self];
	}	
	// Has he been asked already? And don't ask if the host has a default value set in its Info.plist.
	else if ([[SUUserDefaults standardUserDefaults] objectForKey:SUEnableAutomaticChecksKey] == nil &&
		[hostBundle objectForInfoDictionaryKey:SUEnableAutomaticChecksKey] == nil)
	{
		if ([[SUUserDefaults standardUserDefaults] objectForKey:SUEnableAutomaticChecksKeyOld])
			[[SUUserDefaults standardUserDefaults] setBool:[[SUUserDefaults standardUserDefaults] boolForKey:SUEnableAutomaticChecksKeyOld] forKey:SUEnableAutomaticChecksKey];
		// Now, we don't want to ask the user for permission to do a weird thing on the first launch.
		// We wait until the second launch.
		else if ([[SUUserDefaults standardUserDefaults] boolForKey:SUHasLaunchedBeforeKey] == NO)
			[[SUUserDefaults standardUserDefaults] setBool:YES forKey:SUHasLaunchedBeforeKey];
		else
			[SUUpdatePermissionPrompt promptWithHostBundle:hostBundle delegate:self];
	}
	
	// We check if the user's said they want updates, or they haven't said anything, and the default is set to checking.
	[self scheduleNextUpdateCheck];
}

- (void)updatePermissionPromptFinishedWithResult:(SUPermissionPromptResult)result
{
	[[SUUserDefaults standardUserDefaults] setBool:(result == SUAutomaticallyCheck) forKey:SUEnableAutomaticChecksKey];
	[self scheduleNextUpdateCheck];
}

- (void)scheduleNextUpdateCheck
{	
	if (checkTimer)
	{
		[checkTimer invalidate];
		checkTimer = nil;
	}
	if (![self shouldScheduleUpdateCheck]) return;
	
	// How long has it been since last we checked for an update?
	NSDate *lastCheckDate = [[SUUserDefaults standardUserDefaults] objectForKey:SULastCheckTimeKey];
	if (!lastCheckDate) { lastCheckDate = [NSDate distantPast]; }
	NSTimeInterval intervalSinceCheck = [[NSDate date] timeIntervalSinceDate:lastCheckDate];
	
	// Now we want to figure out how long until we check again.
	NSTimeInterval delayUntilCheck;
	if (intervalSinceCheck < [self checkInterval])
		delayUntilCheck = ([self checkInterval] - intervalSinceCheck); // It hasn't been long enough.
	else
		delayUntilCheck = 0; // We're overdue! Run one now.
	checkTimer = [NSTimer scheduledTimerWithTimeInterval:delayUntilCheck target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:NO];
}

- (void)checkForUpdatesInBackground
{
	[self checkForUpdatesWithDriver:[[[([self automaticallyUpdates] ? [SUAutomaticUpdateDriver class] : [SUScheduledUpdateDriver class]) alloc] init] autorelease]];
}

- (IBAction)checkForUpdates:sender
{
	[self checkForUpdatesWithDriver:[[[SUUserInitiatedUpdateDriver alloc] init] autorelease]];
}

- (void)checkForUpdatesWithDriver:(SUUpdateDriver *)d
{
	if ([self updateInProgress]) { return; }
	if (checkTimer) { [checkTimer invalidate]; checkTimer = nil; }
	
	driver = [d retain];
	if ([driver delegate] == nil) { [driver setDelegate:delegate]; }
	[driver checkForUpdatesAtURL:[self feedURL] hostBundle:hostBundle];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	if (object == [NSUserDefaultsController sharedUserDefaultsController] && ([keyPath hasSuffix:SUScheduledCheckIntervalKey] || [keyPath hasSuffix:SUEnableAutomaticChecksKey]))
	{
		[self updatePreferencesChanged];
	}
	else
	{
		[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
	}
}

- (void)updatePreferencesChanged
{
	[self scheduleNextUpdateCheck];
}

- (BOOL)shouldScheduleUpdateCheck
{
	// Breaking this down for readability:
	// If the user says he wants automatic update checks, let's do it.
	if ([[SUUserDefaults standardUserDefaults] boolForKey:SUEnableAutomaticChecksKey] == YES)
		return YES;
	// If the user hasn't said anything, but the developer says we should do it, let's do it.
	if ([[SUUserDefaults standardUserDefaults] objectForKey:SUEnableAutomaticChecksKey] == nil &&
	  [[hostBundle objectForInfoDictionaryKey:SUEnableAutomaticChecksKey] boolValue] == YES)
		return YES;
	return NO; // Otherwise, don't bother.
}

- (BOOL)automaticallyUpdates
{
	// If the SUAllowsAutomaticUpdatesKey exists and is set to NO, return NO.
	if ([hostBundle objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] &&
		[[hostBundle objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] boolValue] == NO)
		return NO;
	
	// If we're not using DSA signatures, we aren't going to trust any updates automatically.
	if ([[hostBundle objectForInfoDictionaryKey:SUExpectsDSASignatureKey] boolValue] != YES)
		return NO;
	
	// If there's no setting, or it's set to no, we're not automatically updating.
	if ([[SUUserDefaults standardUserDefaults] boolForKey:SUAutomaticallyUpdateKey] != YES)
		return NO;
	
	return YES; // Otherwise, we're good to go.
}

- (NSString *)installationId
{
    NSString *uuid = [[NSUserDefaults standardUserDefaults] valueForKey:SUInstallationIdKey];

    if (uuid == nil) {
        uuid_t buffer;
        
        uuid_generate(buffer);

        char str[36];

        uuid_unparse_upper(buffer, str);
        
        uuid = [NSString stringWithFormat:@"%s", str];

        [[NSUserDefaults standardUserDefaults] setValue: uuid
                                                 forKey: SUInstallationIdKey];

    }
    
    return uuid;
}

- (NSURL *)_baseFeedURL
{
	// A value in the user defaults overrides one in the Info.plist (so preferences panels can be created wherein users choose between beta / release feeds).
	NSString *appcastString = [[SUUserDefaults standardUserDefaults] objectForKey:SUFeedURLKey];
	if (!appcastString)
		appcastString = [hostBundle objectForInfoDictionaryKey:SUFeedURLKey];
	if (!appcastString) // Can't find an appcast string!
		[NSException raise:@"SUNoFeedURL" format:@"You must specify the URL of the appcast as the SUFeedURLKey in either the Info.plist or the user defaults!"];
	NSCharacterSet* quoteSet = [NSCharacterSet characterSetWithCharactersInString: @"\"\'"]; // Some feed publishers add quotes; strip 'em.
	return [NSURL URLWithString:[appcastString stringByTrimmingCharactersInSet:quoteSet]] ;
}

- (NSURL *)feedURL
{
	NSURL *baseFeedURL = [self _baseFeedURL];
	
	// Determine all the parameters we're attaching to the base feed URL.
	BOOL sendingSystemProfile = ([[SUUserDefaults standardUserDefaults] boolForKey:SUSendProfileInfoKey] == YES);
	BOOL sendingInstallationId = ([[SUUserDefaults standardUserDefaults] boolForKey:SUSendInstallationIdKey] == YES);
	NSArray *parameters = [NSArray array];
	if ([delegate respondsToSelector:@selector(feedParametersForHostBundle:sendingSystemProfile:)])
		parameters = [parameters arrayByAddingObjectsFromArray:[delegate feedParametersForHostBundle:hostBundle sendingSystemProfile:sendingSystemProfile]];
    if (sendingInstallationId)
        parameters = [parameters arrayByAddingObject:[NSDictionary dictionaryWithObject:[self installationId] forKey:@"uuid"]];
	if (sendingSystemProfile)
		parameters = [parameters arrayByAddingObjectsFromArray:[hostBundle systemProfile]];
	if (parameters == nil || [parameters count] == 0) { return baseFeedURL; }
	
	// Build up the parameterized URL.
	NSMutableArray *parameterStrings = [NSMutableArray array];
	NSEnumerator *profileInfoEnumerator = [parameters objectEnumerator];
	NSDictionary *currentProfileInfo;
	while ((currentProfileInfo = [profileInfoEnumerator nextObject]))
		[parameterStrings addObject:[NSString stringWithFormat:@"%@=%@", [currentProfileInfo objectForKey:@"key"], [currentProfileInfo objectForKey:@"value"]]];
	
	NSString *appcastStringWithProfile = [NSString stringWithFormat:@"%@?%@", [baseFeedURL absoluteString], [parameterStrings componentsJoinedByString:@"&"]];
	
	// Clean it up so it's a valid URL
	return [NSURL URLWithString:[appcastStringWithProfile stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
}

- (NSTimeInterval)checkInterval
{
	NSTimeInterval checkInterval = 0;
	// Find the stored check interval. User defaults override Info.plist.
	if ([[SUUserDefaults standardUserDefaults] objectForKey:SUScheduledCheckIntervalKey])
		checkInterval = [[[SUUserDefaults standardUserDefaults] objectForKey:SUScheduledCheckIntervalKey] doubleValue];
	else if ([hostBundle objectForInfoDictionaryKey:SUScheduledCheckIntervalKey])
		checkInterval = [[hostBundle objectForInfoDictionaryKey:SUScheduledCheckIntervalKey] doubleValue];
	
	if (checkInterval < SU_MIN_CHECK_INTERVAL) // This can also mean one that isn't set.
		checkInterval = SU_DEFAULT_CHECK_INTERVAL;	
	return checkInterval;
}

- (void)dealloc
{
	[hostBundle release];
	if (checkTimer) { [checkTimer invalidate]; }
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	[super dealloc];
}

- (BOOL)validateMenuItem:(NSMenuItem *)item
{
	if ([item action] == @selector(checkForUpdates:))
		return ![self updateInProgress];
	return YES;
}

- (void)setDelegate:aDelegate
{
	delegate = aDelegate;
}

- (void)setHostBundle:(NSBundle *)hb
{
	if (hostBundle == hb) return;
	[hostBundle release];
	hostBundle = [hb retain];
	[[SUUserDefaults standardUserDefaults] setIdentifier:[hostBundle bundleIdentifier]];
}

- (BOOL)updateInProgress
{
	return driver && ([driver finished] == NO);
}

- (void)driverDidFinish:(NSNotification *)notification
{
	if ([notification object] != driver) return;
	[driver release];
	driver = nil;
	[self scheduleNextUpdateCheck];
}

@end