source: trunk/Sparkle/SUUpdater.m @ 1046

Revision 992, 15.3 KB checked in by speck, 13 months ago (diff)

Prefer CFBundleShortVersionString from LS in Sparkle info. Fix bugs in Sparkle that made it send semi-garbled UA string.

Line 
1//
2//  SUUpdater.m
3//  Sparkle
4//
5//  Created by Andy Matuschak on 1/4/06.
6//  Copyright 2006 Andy Matuschak. All rights reserved.
7//
8
9#import "SUUpdater.h"
10
11#import "SUHost.h"
12#import "SUUpdatePermissionPrompt.h"
13
14#import "SUAutomaticUpdateDriver.h"
15#import "SUProbingUpdateDriver.h"
16#import "SUUserInitiatedUpdateDriver.h"
17#import "SUScheduledUpdateDriver.h"
18
19@interface SUUpdater (Private)
20- initForBundle:(NSBundle *)bundle;
21- (void)startUpdateCycle;
22- (void)checkForUpdatesWithDriver:(SUUpdateDriver *)updateDriver;
23- (BOOL)automaticallyDownloadsUpdates;
24- (void)scheduleNextUpdateCheck;
25- (void)registerAsObserver;
26- (void)unregisterAsObserver;
27- (void)updateDriverDidFinish:(NSNotification *)note;
28- initForBundle:(NSBundle *)bundle;
29- (NSURL *)parameterizedFeedURL;
30@end
31
32@implementation SUUpdater
33
34#pragma mark Initialization
35
36static NSMutableDictionary *sharedUpdaters = nil;
37static NSString *SUUpdaterDefaultsObservationContext = @"SUUpdaterDefaultsObservationContext";
38
39+ (SUUpdater *)sharedUpdater
40{
41    return [self updaterForBundle:[NSBundle mainBundle]];
42}
43
44// SUUpdater has a singleton for each bundle. We use the fact that NSBundle instances are also singletons, so we can use them as keys. If you don't trust that you can also use the identifier as key
45+ (SUUpdater *)updaterForBundle:(NSBundle *)bundle
46{
47    if (bundle == nil) bundle = [NSBundle mainBundle];
48    id updater = [sharedUpdaters objectForKey:[NSValue valueWithNonretainedObject:bundle]];
49    if (updater == nil)
50        updater = [[[self class] alloc] initForBundle:bundle];
51    return updater;
52}
53
54// This is the designated initializer for SUUpdater, important for subclasses
55- initForBundle:(NSBundle *)bundle
56{
57    self = [super init];
58    if (bundle == nil) bundle = [NSBundle mainBundle];
59    id updater = [sharedUpdaters objectForKey:[NSValue valueWithNonretainedObject:bundle]];
60    if (updater)
61    {
62        [self release];
63        self = [updater retain];
64    }
65    else if (self)
66    {
67        if (sharedUpdaters == nil)
68            sharedUpdaters = [[NSMutableDictionary alloc] init];
69        [sharedUpdaters setObject:self forKey:[NSValue valueWithNonretainedObject:bundle]];
70        host = [[SUHost alloc] initWithBundle:bundle];
71        [self registerAsObserver];
72        // This runs the permission prompt if needed, but never before the app has finished launching because the runloop won't run before that
73        [self performSelector:@selector(startUpdateCycle) withObject:nil afterDelay:0];
74    }
75    return self;
76}
77
78// This will be used when the updater is instantiated in a nib such as MainMenu
79- (id)init
80{
81    return [self initForBundle:[NSBundle mainBundle]];
82}
83
84- (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], [host bundlePath]]; }
85
86- (void)startUpdateCycle
87{
88    BOOL shouldPrompt = NO;
89   
90    // If the user has been asked about automatic checks, don't bother prompting
91    if ([host objectForUserDefaultsKey:SUEnableAutomaticChecksKey])
92    {
93        shouldPrompt = NO;
94    }
95    // Does the delegate want to take care of the logic for when we should ask permission to update?
96    else if ([delegate respondsToSelector:@selector(updaterShouldPromptForPermissionToCheckForUpdates:)])
97    {
98        shouldPrompt = [delegate updaterShouldPromptForPermissionToCheckForUpdates:self];
99    }   
100    // Has he been asked already? And don't ask if the host has a default value set in its Info.plist.
101    else if ([host objectForKey:SUEnableAutomaticChecksKey] == nil)
102    {
103        if ([host objectForUserDefaultsKey:SUEnableAutomaticChecksKeyOld])
104            [self setAutomaticallyChecksForUpdates:[host boolForUserDefaultsKey:SUEnableAutomaticChecksKeyOld]];
105        // Now, we don't want to ask the user for permission to do a weird thing on the first launch.
106        // We wait until the second launch.
107        else if ([host boolForUserDefaultsKey:SUHasLaunchedBeforeKey] == NO)
108            [host setBool:YES forUserDefaultsKey:SUHasLaunchedBeforeKey];
109        else
110            shouldPrompt = YES;
111    }
112   
113    if (shouldPrompt)
114    {
115        NSArray *profileInfo = [host systemProfile];
116        if ([delegate respondsToSelector:@selector(feedParametersForUpdater:sendingSystemProfile:)])
117            profileInfo = [profileInfo arrayByAddingObjectsFromArray:[delegate feedParametersForUpdater:self sendingSystemProfile:[self sendsSystemProfile]]];     
118        [SUUpdatePermissionPrompt promptWithHost:host systemProfile:profileInfo delegate:self];
119        // We start the update checks and register as observer for changes after the prompt finishes
120    }
121    else
122    {
123        // We check if the user's said they want updates, or they haven't said anything, and the default is set to checking.
124        [self scheduleNextUpdateCheck];
125    }
126}
127
128- (void)updatePermissionPromptFinishedWithResult:(SUPermissionPromptResult)result
129{
130    [self setAutomaticallyChecksForUpdates:(result == SUAutomaticallyCheck)];
131    // Schedule checks, but make sure we ignore the delayed call from KVO
132    [self resetUpdateCycle];
133}
134
135- (void)updateDriverDidFinish:(NSNotification *)note
136{
137    if ([note object] == driver && [driver finished])
138    {
139        [driver release]; driver = nil;
140        [self scheduleNextUpdateCheck];
141    }
142}
143
144- (NSDate *)lastUpdateCheckDate
145{
146    return [host objectForUserDefaultsKey:SULastCheckTimeKey];
147}
148
149- (void)scheduleNextUpdateCheck
150{   
151    if (checkTimer)
152    {
153        [checkTimer invalidate];
154        checkTimer = nil;
155    }
156    if (![self automaticallyChecksForUpdates]) return;
157   
158    // How long has it been since last we checked for an update?
159    NSDate *lastCheckDate = [self lastUpdateCheckDate];
160    if (!lastCheckDate) { lastCheckDate = [NSDate distantPast]; }
161    NSTimeInterval intervalSinceCheck = [[NSDate date] timeIntervalSinceDate:lastCheckDate];
162   
163    // Now we want to figure out how long until we check again.
164    NSTimeInterval delayUntilCheck, updateCheckInterval = [self updateCheckInterval];
165    if (updateCheckInterval < SU_MIN_CHECK_INTERVAL)
166        updateCheckInterval = SU_MIN_CHECK_INTERVAL;
167    if (intervalSinceCheck < updateCheckInterval)
168        delayUntilCheck = (updateCheckInterval - intervalSinceCheck); // It hasn't been long enough.
169    else
170        delayUntilCheck = 0; // We're overdue! Run one now.
171    checkTimer = [NSTimer scheduledTimerWithTimeInterval:delayUntilCheck target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:NO];
172}
173
174- (void)checkForUpdatesInBackground
175{
176    [self checkForUpdatesWithDriver:[[[([self automaticallyDownloadsUpdates] ? [SUAutomaticUpdateDriver class] : [SUScheduledUpdateDriver class]) alloc] initWithUpdater:self] autorelease]];
177}
178
179- (IBAction)checkForUpdates:sender
180{
181    [self checkForUpdatesWithDriver:[[[SUUserInitiatedUpdateDriver alloc] initWithUpdater:self] autorelease]];
182}
183
184- (void)checkForUpdateInformation
185{
186    [self checkForUpdatesWithDriver:[[[SUProbingUpdateDriver alloc] initWithUpdater:self] autorelease]];
187}
188
189- (void)checkForUpdatesWithDriver:(SUUpdateDriver *)d
190{
191    if ([self updateInProgress]) { return; }
192    if (checkTimer) { [checkTimer invalidate]; checkTimer = nil; }
193       
194    [self willChangeValueForKey:@"lastUpdateCheckDate"];
195    [host setObject:[NSDate date] forUserDefaultsKey:SULastCheckTimeKey];
196    [self didChangeValueForKey:@"lastUpdateCheckDate"];
197   
198    driver = [d retain];
199    [driver checkForUpdatesAtURL:[self parameterizedFeedURL] host:host];
200}
201
202- (void)registerAsObserver
203{
204    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDriverDidFinish:) name:SUUpdateDriverFinishedNotification object:nil];
205    // No sense observing the shared NSUserDefaultsController when we're not updating the main bundle.
206    if ([host bundle] != [NSBundle mainBundle]) return;
207    [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:[@"values." stringByAppendingString:SUScheduledCheckIntervalKey] options:0 context:SUUpdaterDefaultsObservationContext];
208    [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:[@"values." stringByAppendingString:SUEnableAutomaticChecksKey] options:0 context:SUUpdaterDefaultsObservationContext];
209}
210
211- (void)unregisterAsObserver
212{
213    [[NSNotificationCenter defaultCenter] removeObserver:self];
214    // Removing self as a KVO observer if no observer was registered leads to an NSException. But we don't care.
215    @try
216    {
217        [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:[@"values." stringByAppendingString:SUScheduledCheckIntervalKey]];
218        [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:[@"values." stringByAppendingString:SUEnableAutomaticChecksKey]];
219    }
220    @catch (NSException *e) { }
221}
222
223- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
224{
225    if (context == SUUpdaterDefaultsObservationContext)
226    {
227        // Allow a small delay, because perhaps the user or developer wants to change both preferences. This allows the developer to interpret a zero check interval as a sign to disable automatic checking.
228        // Or we may get this from the developer and from our own KVO observation, this will effectively coalesce them.
229        [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetUpdateCycle) object:nil];
230        [self performSelector:@selector(resetUpdateCycle) withObject:nil afterDelay:1];
231    }
232    else
233    {
234        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
235    }
236}
237
238- (void)resetUpdateCycle
239{
240    [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetUpdateCycle) object:nil];
241    [self scheduleNextUpdateCheck];
242}
243
244- (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyCheckForUpdates
245{
246    [host setBool:automaticallyCheckForUpdates forUserDefaultsKey:SUEnableAutomaticChecksKey];
247    // Hack to support backwards compatibility with older Sparkle versions, which supported
248    // disabling updates by setting the check interval to 0.
249    if (automaticallyCheckForUpdates && [self updateCheckInterval] == 0)
250        [self setUpdateCheckInterval:SU_DEFAULT_CHECK_INTERVAL];
251    [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetUpdateCycle) object:nil];
252    // Provide a small delay in case multiple preferences are being updated simultaneously.
253    [self performSelector:@selector(resetUpdateCycle) withObject:nil afterDelay:1];
254}
255
256- (BOOL)automaticallyChecksForUpdates
257{
258    // Don't automatically update when the check interval is 0, to be compatible with 1.1 settings.
259    if ([self updateCheckInterval] == 0)
260        return NO; 
261    return [host boolForKey:SUEnableAutomaticChecksKey];
262}
263
264- (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyUpdates
265{
266    [host setBool:automaticallyUpdates forUserDefaultsKey:SUAutomaticallyUpdateKey];
267}
268
269- (BOOL)automaticallyDownloadsUpdates
270{
271    // If the SUAllowsAutomaticUpdatesKey exists and is set to NO, return NO.
272    if ([host objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] && [host boolForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] == NO)
273        return NO;
274   
275    // Otherwise, automatically downloading updates is allowed. Does the user want it?
276    return [host boolForUserDefaultsKey:SUAutomaticallyUpdateKey];
277}
278
279- (void)setFeedURL:(NSURL *)feedURL
280{
281    [host setObject:[feedURL absoluteString] forUserDefaultsKey:SUFeedURLKey];
282}
283
284- (NSURL *)feedURL
285{
286    // 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).
287    NSString *appcastString = [host objectForKey:SUFeedURLKey];
288    if (!appcastString) // Can't find an appcast string!
289        [NSException raise:@"SUNoFeedURL" format:@"You must specify the URL of the appcast as the SUFeedURLKey in either the Info.plist or the user defaults!"];
290    NSCharacterSet* quoteSet = [NSCharacterSet characterSetWithCharactersInString: @"\"\'"]; // Some feed publishers add quotes; strip 'em.
291    return [NSURL URLWithString:[appcastString stringByTrimmingCharactersInSet:quoteSet]];
292}
293
294- (void)setSendsSystemProfile:(BOOL)sendsSystemProfile
295{
296    [host setBool:sendsSystemProfile forUserDefaultsKey:SUSendProfileInfoKey];
297}
298
299- (BOOL)sendsSystemProfile
300{
301    return [host boolForUserDefaultsKey:SUSendProfileInfoKey];
302}
303
304- (NSURL *)parameterizedFeedURL
305{
306    NSURL *baseFeedURL = [self feedURL];
307   
308    // Determine all the parameters we're attaching to the base feed URL.
309    BOOL sendingSystemProfile = [self sendsSystemProfile];
310
311    // Let's only send the system profiling information once per week at most, so we normalize daily-checkers vs. biweekly-checkers and the such.
312    NSDate *lastSubmitDate = [host objectForUserDefaultsKey:SULastProfileSubmitDateKey];
313    if(!lastSubmitDate)
314        lastSubmitDate = [NSDate distantPast];
315    const NSTimeInterval oneWeek = 60 * 60 * 24 * 7;
316    sendingSystemProfile &= (-[lastSubmitDate timeIntervalSinceNow] >= oneWeek);
317
318    NSArray *parameters = [NSArray array];
319    if ([delegate respondsToSelector:@selector(feedParametersForUpdater:sendingSystemProfile:)])
320        parameters = [parameters arrayByAddingObjectsFromArray:[delegate feedParametersForUpdater:self sendingSystemProfile:sendingSystemProfile]];
321    if (sendingSystemProfile)
322    {
323        parameters = [parameters arrayByAddingObjectsFromArray:[host systemProfile]];
324        [host setObject:[NSDate date] forUserDefaultsKey:SULastProfileSubmitDateKey];
325    }
326    if (parameters == nil || [parameters count] == 0) { return baseFeedURL; }
327   
328    // Build up the parameterized URL.
329    NSMutableArray *parameterStrings = [NSMutableArray arrayWithCapacity:10];
330    NSEnumerator *profileInfoEnumerator = [parameters objectEnumerator];
331    NSDictionary *currentProfileInfo;
332    while ((currentProfileInfo = [profileInfoEnumerator nextObject]))
333        [parameterStrings addObject:[NSString stringWithFormat:@"%@=%@", [currentProfileInfo objectForKey:@"key"], [currentProfileInfo objectForKey:@"value"]]];
334   
335    NSString *separatorCharacter = @"?";
336    if ([baseFeedURL query])
337        separatorCharacter = @"&"; // In case the URL is already http://foo.org/baz.xml?bat=4
338    NSString *appcastStringWithProfile = [NSString stringWithFormat:@"%@%@%@", [baseFeedURL absoluteString], separatorCharacter, [parameterStrings componentsJoinedByString:@"&"]];
339   
340    // Clean it up so it's a valid URL
341    return [NSURL URLWithString:[appcastStringWithProfile stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
342}
343
344- (void)setUpdateCheckInterval:(NSTimeInterval)updateCheckInterval
345{
346    [host setObject:[NSNumber numberWithDouble:updateCheckInterval] forUserDefaultsKey:SUScheduledCheckIntervalKey];
347    if (updateCheckInterval == 0) // For compatibility with 1.1's settings.
348        [self setAutomaticallyChecksForUpdates:NO];
349    [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetUpdateCycle) object:nil];
350   
351    // Provide a small delay in case multiple preferences are being updated simultaneously.
352    [self performSelector:@selector(resetUpdateCycle) withObject:nil afterDelay:1];
353}
354
355- (NSTimeInterval)updateCheckInterval
356{
357    // Find the stored check interval. User defaults override Info.plist.
358    NSNumber *intervalValue = [host objectForKey:SUScheduledCheckIntervalKey];
359    if (intervalValue)
360        return [intervalValue doubleValue];
361    else
362        return SU_DEFAULT_CHECK_INTERVAL;
363}
364
365- (void)dealloc
366{
367    [self unregisterAsObserver];
368    [host release];
369    if (checkTimer) { [checkTimer invalidate]; }
370    [super dealloc];
371}
372
373- (BOOL)validateMenuItem:(NSMenuItem *)item
374{
375    if ([item action] == @selector(checkForUpdates:))
376        return ![self updateInProgress];
377    return YES;
378}
379
380- (void)setDelegate:aDelegate
381{
382    delegate = aDelegate;
383}
384
385- (BOOL)updateInProgress
386{
387    return driver && ([driver finished] == NO);
388}
389
390- delegate { return delegate; }
391- (NSBundle *)hostBundle { return [host bundle]; }
392
393@end
Note: See TracBrowser for help on using the repository browser.