diff --git a/Downloader/SPUDownloader.m b/Downloader/SPUDownloader.m index 59126a30..c4225145 100644 --- a/Downloader/SPUDownloader.m +++ b/Downloader/SPUDownloader.m @@ -11,6 +11,7 @@ #import "SPULocalCacheDirectory.h" #import "SPUURLRequest.h" #import "SPUDownloadData.h" +#import "SPUDownloadDataPrivate.h" #import "SUErrors.h" diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 6c29351d..034172d9 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -939,6 +939,7 @@ 7218EC362623F31C008FECF3 /* org.sparkle-project.InstallerConnection.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "org.sparkle-project.InstallerConnection.entitlements"; sourceTree = ""; }; 7218EC372623F32E008FECF3 /* org.sparkle-project.InstallerStatus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "org.sparkle-project.InstallerStatus.entitlements"; sourceTree = ""; }; 7218EC4B2623F51C008FECF3 /* codesign_xpc_service.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = codesign_xpc_service.py; sourceTree = ""; }; + 721AB11626C777D900D34A86 /* SPUDownloadDataPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUDownloadDataPrivate.h; sourceTree = ""; }; 721BC2061D17A532002BC71E /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 721BC2081D17A553002BC71E /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 721BC20A1D17A5AD002BC71E /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; @@ -1780,6 +1781,7 @@ isa = PBXGroup; children = ( 72F9EC421D5E9ED8004AC8B6 /* SPUDownloadData.h */, + 721AB11626C777D900D34A86 /* SPUDownloadDataPrivate.h */, 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */, 7214B8851D45AD9A00CB5CED /* SPUInstallationType.h */, 7293A1AC1CEE933800B957A7 /* SPUURLRequest.h */, diff --git a/Sparkle/SPUDownloadData.h b/Sparkle/SPUDownloadData.h index d0ca9935..ead146cf 100644 --- a/Sparkle/SPUDownloadData.h +++ b/Sparkle/SPUDownloadData.h @@ -32,8 +32,6 @@ NS_ASSUME_NONNULL_BEGIN */ SU_EXPORT @interface SPUDownloadData : NSObject -- (instancetype)initWithData:(NSData *)data URL:(NSURL *)URL textEncodingName:(NSString * _Nullable)textEncodingName MIMEType:(NSString * _Nullable)MIMEType; - /** * The raw data that was downloaded. */ diff --git a/Sparkle/SPUDownloadDataPrivate.h b/Sparkle/SPUDownloadDataPrivate.h new file mode 100644 index 00000000..e0cf7ef1 --- /dev/null +++ b/Sparkle/SPUDownloadDataPrivate.h @@ -0,0 +1,19 @@ +// +// SPUDownloadDataPrivate.h +// SPUDownloadDataPrivate +// +// Created by Mayur Pawashe on 8/13/21. +// Copyright © 2021 Sparkle Project. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SPUDownloadData (Private) + +- (instancetype)initWithData:(NSData *)data URL:(NSURL *)URL textEncodingName:(NSString * _Nullable)textEncodingName MIMEType:(NSString * _Nullable)MIMEType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle/SPUInstallerDriver.m b/Sparkle/SPUInstallerDriver.m index 56a0c06b..ac9b9648 100644 --- a/Sparkle/SPUInstallerDriver.m +++ b/Sparkle/SPUInstallerDriver.m @@ -12,7 +12,7 @@ #import "SPUXPCServiceInfo.h" #import "SPUUpdaterDelegate.h" #import "SUAppcastItem.h" -#import "SULog.h" +#import "SUAppcastItem+Private.h" #import "SULocalizations.h" #import "SUErrors.h" #import "SUHost.h" diff --git a/Sparkle/SPUStandardUserDriver.h b/Sparkle/SPUStandardUserDriver.h index 9e797957..f2ad0881 100644 --- a/Sparkle/SPUStandardUserDriver.h +++ b/Sparkle/SPUStandardUserDriver.h @@ -30,7 +30,7 @@ SU_EXPORT @interface SPUStandardUserDriver : NSObject Initializes a Sparkle's standard user driver for user update interactions @param hostBundle The target bundle of the host that is being updated. - @param delegate The delegate to this user driver. Pass nil if you don't want to provide one. + @param delegate The optional delegate to this user driver. */ - (instancetype)initWithHostBundle:(NSBundle *)hostBundle delegate:(nullable id)delegate; diff --git a/Sparkle/SPUUpdater.h b/Sparkle/SPUUpdater.h index cad560b8..f4e8d030 100644 --- a/Sparkle/SPUUpdater.h +++ b/Sparkle/SPUUpdater.h @@ -220,7 +220,7 @@ SU_EXPORT @interface SPUUpdater : NSObject The user agent used when checking for updates. By default the user agent string returned is in the format: - $(BundleDisplayName)/$(BundleDisplayVersion)/$(SparkleDisplayVersion) + $(BundleDisplayName)/$(BundleDisplayVersion) Sparkle/$(SparkleDisplayVersion) BundleDisplayVersion is derived from the application Info.plist's CFBundleShortVersionString. diff --git a/Sparkle/SPUUpdater.m b/Sparkle/SPUUpdater.m index 90a0ea02..e9ea6843 100644 --- a/Sparkle/SPUUpdater.m +++ b/Sparkle/SPUUpdater.m @@ -403,8 +403,8 @@ NSString *const SUUpdaterAppcastNotificationKey = @"SUUpdaterAppCastNotification [self.updaterTimer invalidate]; if (![self automaticallyChecksForUpdates]) { - if ([self.delegate respondsToSelector:@selector(updaterWillIdleSchedulingUpdates:)]) { - [self.delegate updaterWillIdleSchedulingUpdates:self]; + if ([self.delegate respondsToSelector:@selector(updaterWillNotScheduleUpdateCheck:)]) { + [self.delegate updaterWillNotScheduleUpdateCheck:self]; } return; } @@ -859,14 +859,16 @@ static NSString *escapeURLComponent(NSString *str) { NSArray *systemProfile = [SUSystemProfiler systemProfileArrayForHost:self.host]; if ([self.delegate respondsToSelector:@selector(allowedSystemProfileKeysForUpdater:)]) { NSArray * allowedKeys = [self.delegate allowedSystemProfileKeysForUpdater:self]; - NSMutableArray *filteredProfile = [NSMutableArray array]; - for (NSDictionary *profileElement in systemProfile) { - NSString *key = [profileElement objectForKey:@"key"]; - if (key && [allowedKeys containsObject:key]) { - [filteredProfile addObject:profileElement]; + if (allowedKeys != nil) { + NSMutableArray *filteredProfile = [NSMutableArray array]; + for (NSDictionary *profileElement in systemProfile) { + NSString *key = [profileElement objectForKey:@"key"]; + if (key && [allowedKeys containsObject:key]) { + [filteredProfile addObject:profileElement]; + } } + systemProfile = [filteredProfile copy]; } - systemProfile = [filteredProfile copy]; } return systemProfile; } diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 9de51c92..bb5ec6ad 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -64,31 +64,11 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) }; /** - Provides methods to control the behavior of an `SPUUpdater` object. + Provides delegation methods to control the behavior of an `SPUUpdater` object. */ @protocol SPUUpdaterDelegate @optional -/** - Called when a background update will be scheduled after a delay. - - Automatic update checks need to be enabled for this to trigger. - - @param delay The delay in seconds until the next scheduled update will occur. - - @param updater The updater instance. - */ -- (void)updater:(SPUUpdater *)updater willScheduleUpdateCheckAfterDelay:(NSTimeInterval)delay; - -/** - Called when no updates will be scheduled in the future. - - This may later change if automatic update checks become enabled. - - @param updater The updater instance. - */ -- (void)updaterWillIdleSchedulingUpdates:(SPUUpdater *)updater; - /** Returns whether to allow Sparkle to pop up. @@ -96,73 +76,25 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) Alternatively, you may want to consider starting the updater after eg: the setup assistant finishes @param updater The updater instance. + @return @c YES if the updater may check for updates otherwise @c NO */ - (BOOL)updaterMayCheckForUpdates:(SPUUpdater *)updater; -/** - Returns additional parameters to append to the appcast URL's query string. - - This is potentially based on whether or not Sparkle will also be sending along the system profile. - - @param updater The updater instance. - @param sendingProfile Whether the system profile will also be sent. - - @return An array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. - */ -- (NSArray *> *)feedParametersForUpdater:(SPUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; - -/** - Returns a list of system profile keys to be appended to the appcast URL's query string. - - If this is unimplemented then all keys will be included. - - @param updater The updater instance. - - @return An array of system profile keys to include in the appcast URL's query string. Elements must be one of the SUSystemProfiler*Key constants - */ -- (NSArray *)allowedSystemProfileKeysForUpdater:(SPUUpdater *)updater; - -/** - Returns a custom appcast URL. - - Override this to dynamically specify the feed URL. - - @param updater The updater instance. - */ -- (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)updater; - -/** - Returns whether Sparkle should prompt the user about automatic update checks. - - Use this to override the default behavior. - - @param updater The updater instance. - */ -- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SPUUpdater *)updater; - -/** - Called after Sparkle has downloaded the appcast from the remote server. - - Implement this if you want to do some special handling with the appcast once it finishes loading. - - @param updater The updater instance. - @param appcast The appcast that was downloaded from the remote server. - */ -- (void)updater:(SPUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; - /** Returns the set of Sparkle channels the updater is allowed to find new updates from. An appcast item can specify a channel the update is posted to. Without specifying a channel, the appcast item is posted to the default channel. For instance: + ``` 2.0 Beta 1 beta + ``` - This example posts an update to the 'beta' channel, so only updaters that are allowed to use the 'beta' channel can find this update. + This example posts an update to the @c beta channel, so only updaters that are allowed to use the @c beta channel can find this update. - If the is not present, the update item is posted to the default channel and can be found by any updater. + If the @c element is not present, the update item is posted to the default channel and can be found by any updater. You can pick any name you'd like for the channel. The valid characters for channel names are letters, numbers, dashes, underscores, and periods. @@ -175,37 +107,61 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) - (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater; /** - Returns the item in the appcast corresponding to the update that should be installed. + Returns a custom appcast URL. - Please consider using or migrating to other supported features before adopting this method. - Specifically: - If you want to filter out certain tagged updates (like beta updates), consider -[SPUUpdaterDelegate allowedChannelsForUpdater:] instead. - If you want to treat certain updates as informational-only, consider supplying with a set of affected versions users are updating from. + Override this to dynamically specify the feed URL. - If you're using special logic or extensions in your appcast, - implement this to use your own logic for finding a valid update, if any, - in the given appcast. - - Do not base your logic by filtering out items with a minimum or maximum OS version or minimum autoupdate version, - because Sparkle already has logic for determining whether or not those items should be filtered out. - Also do not return a non-top level item from the appcast such as a delta item. Delta items will be ignored. - Sparkle picks the delta item from your selection if the appropriate one is available. - - This method will not be invoked with an appcast that has zero items. Pick the best item from the appcast. - If an item is available that has the same version as the application or bundle to update, - do not pick an item that is worse than that version. - - This method may be called multiple times for different selections and filters. This method should be efficient. - - @param appcast The appcast that was downloaded from the remote server. @param updater The updater instance. - @return The best valid appcast item. Return SUAppcastItem.emptyAppcastItem if no appcast item is valid. Return nil if you don't want to be delegated this task and - want to let Sparkle handle picking the best valid update. + @return An appcast feed URL to check for new updates in. Return @c nil for the default behavior and if you don't want to be delegated this task. */ -- (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SPUUpdater *)updater; +- (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)updater; /** - Called when a valid update is found by the update driver. + Returns additional parameters to append to the appcast URL's query string. + + This is potentially based on whether or not Sparkle will also be sending along the system profile. + + @param updater The updater instance. + @param sendingProfile Whether the system profile will also be sent. + + @return An array of dictionaries with keys: `key`, `value`, `displayKey`, `displayValue`, the latter two being specifically for display to the user. + */ +- (NSArray *> *)feedParametersForUpdater:(SPUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; + + +/** + Returns whether Sparkle should prompt the user about automatic update checks. + + Use this to override the default behavior. + + @param updater The updater instance. + @return @c YES if the updater should prompt for permission to check for updates, otherwise @c NO + */ +- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SPUUpdater *)updater; + +/** + Returns an allowed list of system profile keys to be appended to the appcast URL's query string. + + By default all keys will be included. This method allows overriding which keys should only be allowed. + + @param updater The updater instance. + + @return An array of system profile keys to include in the appcast URL's query string. Elements must be one of the `SUSystemProfiler*Key` constants. Return @c nil for the default behavior and if you don't want to be delegated this task. + */ +- (nullable NSArray *)allowedSystemProfileKeysForUpdater:(SPUUpdater *)updater; + +/** + Called after Sparkle has downloaded the appcast from the remote server. + + Implement this if you want to do some special handling with the appcast once it finishes loading. + + @param updater The updater instance. + @param appcast The appcast that was downloaded from the remote server. + */ +- (void)updater:(SPUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; + +/** + Called when a new valid update is found by the update driver. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. @@ -213,26 +169,61 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) - (void)updater:(SPUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; /** - Called when a valid update is not found. + Called when a valid new update is not found. + + There are various reasons a new update is unavailable and can't be installed. + + The userInfo dictionary on the error is populated with three keys: + - `SPULatestAppcastItemFoundKey`: if available, this may provide the latest `SUAppcastItem` that was found. This will be @c nil if it's unavailable. + - `SPUNoUpdateFoundReasonKey`: This will provide the `SPUNoUpdateFoundReason`. + For example the reason could be because the latest version in the feed requires a newer OS version or could be because the user is already on the latest version. + - `SPUNoUpdateFoundUserInitiatedKey`: A boolean that indicates if a new update was not found when the user intitiated an update check manually. @param updater The updater instance. @param error An error containing information on why a new valid update was not found - There are various reasons a new update is unavailable and can't be installed. - The userInfo dictionary on the error is also populated with three keys: - SPULatestAppcastItemFoundKey: if available, this may provide the latest SUAppcastItem that was found. This will be nil if it's unavailable. - SPUNoUpdateFoundReasonKey: This will provide the SUNoUpdateFoundReason. - For example the reason could be because the latest version in the feed requires a newer OS version or could be because the user is already on the latest version. - SPUNoUpdateFoundUserInitiatedKey: A boolean that indicates if a new update was not found when the user intitiated an update check manually. */ - (void)updaterDidNotFindUpdate:(SPUUpdater *)updater error:(NSError *)error; /** - Called when a valid update is not found. + Called when a valid new update is not found. + + If more information is needed on why an update was not found, use `-[SPUUpdaterDelegate updaterDidNotFindUpdate:error:]` instead. @param updater The updater instance. */ - (void)updaterDidNotFindUpdate:(SPUUpdater *)updater; +/** + Returns the item in the appcast corresponding to the update that should be installed. + + Please consider using or migrating to other supported features before adopting this method. + Specifically: + - If you want to filter out certain tagged updates (like beta updates), consider `-[SPUUpdaterDelegate allowedChannelsForUpdater:]` instead. + - If you want to treat certain updates as informational-only, consider supplying @c with a set of affected versions users are updating from. + + If you're using special logic or extensions in your appcast, implement this to use your own logic for finding a valid update, if any, in the given appcast. + + Do not base your logic by filtering out items with a minimum or maximum OS version or minimum autoupdate version + because Sparkle already has logic for determining whether or not those items should be filtered out. + + Also do not return a non-top level item from the appcast such as a delta item. Delta items will be ignored. + Sparkle picks the delta item from your selection if the appropriate one is available. + + This method will not be invoked with an appcast that has zero items. Pick the best item from the appcast. + If an item is available that has the same version as the application or bundle to update, do not pick an item that is worse than that version. + + This method may be called multiple times for different selections and filters. This method should be efficient. + + Return `+[SUAppcastItem emptyAppcastItem]` if no appcast item is valid. + + Return @c nil if you don't want to be delegated this task and want to let Sparkle handle picking the best valid update. + + @param appcast The appcast that was downloaded from the remote server. + @param updater The updater instance. + @return The best valid appcast item. + */ +- (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SPUUpdater *)updater; + /** Called when an update is skipped by the user. @@ -244,11 +235,11 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) /** Returns whether the release notes (if available) should be downloaded after an update is found and shown. - This is specifically for the releaseNotesLink element in the appcast. + This is specifically for the @c element in the appcast item. @param updater The updater instance. - @return @c YES to download and show the release notes if available, otherwise @c NO. The default behavior is YES. + @return @c YES to download and show the release notes if available, otherwise @c NO. The default behavior is @c YES. */ - (BOOL)updaterShouldDownloadReleaseNotes:(SPUUpdater *)updater; @@ -328,11 +319,11 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) /** Returns whether the application should be relaunched at all. - Some apps \b cannot be relaunched under certain circumstances. + Some apps @b cannot be relaunched under certain circumstances. This method can be used to explicitly prevent a relaunch. @param updater The updater instance. - @return YES if the updater should be relaunched, otherwise NO if it shouldn't. + @return @c YES if the updater should be relaunched, otherwise @c NO if it shouldn't. */ - (BOOL)updaterShouldRelaunchApplication:(SPUUpdater *)updater; @@ -354,10 +345,30 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) even if you provide a custom comparator here. @param updater The updater instance. - @return The custom version comparator or nil if you don't want to be delegated this task. + @return The custom version comparator or @c nil if you don't want to be delegated this task. */ - (nullable id)versionComparatorForUpdater:(SPUUpdater *)updater; +/** + Called when a background update will be scheduled after a delay. + + Automatic update checks need to be enabled for this to trigger. + + @param delay The delay in seconds until the next scheduled update will occur. + + @param updater The updater instance. + */ +- (void)updater:(SPUUpdater *)updater willScheduleUpdateCheckAfterDelay:(NSTimeInterval)delay; + +/** + Called when no update checks will be scheduled in the future. + + This may later change if automatic update checks become enabled. + + @param updater The updater instance. + */ +- (void)updaterWillNotScheduleUpdateCheck:(SPUUpdater *)updater; + /** Returns whether or not the updater should allow interaction from the installer @@ -380,24 +391,26 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) /** Returns the decryption password (if any) which is used to extract the update archive DMG. - Return nil if no password should be used. + Return @c nil if no password should be used. @param updater The updater instance. - @return The password used for decrypting the archive, or nil if no password should be used. + @return The password used for decrypting the archive, or @c nil if no password should be used. */ - (nullable NSString *)decryptionPasswordForUpdater:(SPUUpdater *)updater; /** Called when an update is scheduled to be silently installed on quit after downloading the update automatically. + If the updater is given responsibility, it can later remind the user an update is available if they have not terminated the application for a long time. + + Also if the updater is given responsibility and the update item is marked critical, the new update will be presented to the user immediately after. + + Even if the @p immediateInstallHandler is not invoked, the installer will attempt to install the update on termination. + @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. @param immediateInstallHandler The install handler to immediately install the update. No UI interaction will be shown and the application will be relaunched after installation. - @return YES if the delegate will handle installing the update or NO if the updater should be given responsibility. - - If the updater is given responsibility, it can later remind the user an update is available if they have not terminated the application for a long time. - Also if the updater is given responsibility and the update item is marked critical, the new update will be presented to the user immediately after. - Even if the immediateInstallHandler is not invoked, the installer will attempt to install the update on termination. + @return @c YES if the delegate will handle installing the update or @c NO if the updater should be given responsibility. */ - (BOOL)updater:(SPUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationBlock:(void (^)(void))immediateInstallHandler; diff --git a/Sparkle/SPUUserUpdateState.h b/Sparkle/SPUUserUpdateState.h index ec13c621..40ee4eba 100644 --- a/Sparkle/SPUUserUpdateState.h +++ b/Sparkle/SPUUserUpdateState.h @@ -22,23 +22,59 @@ NS_ASSUME_NONNULL_BEGIN +/** + A choice made by the user when prompted with a new update. + */ typedef NS_ENUM(NSInteger, SPUUserUpdateChoice) { + /** + Dismisses the update and skips being notified of it in the future. + */ SPUUserUpdateChoiceSkip, + /** + Downloads (if needed) and installs the update. + */ SPUUserUpdateChoiceInstall, + /** + Dismisses the update until Sparkle reminds the user of it at a later time. + */ SPUUserUpdateChoiceDismiss, }; +/** + Describes the current stage an update is undergoing. + */ typedef NS_ENUM(NSInteger, SPUUserUpdateStage) { + /** + The update has not been downloaded. + */ SPUUserUpdateStageNotDownloaded, + /** + The update has already been downloaded but not begun installing. + */ SPUUserUpdateStageDownloaded, + /** + The update has already been downloaded and began installing in the background. + */ SPUUserUpdateStageInstalling }; +/** + This represents the user's current update state. + */ SU_EXPORT @interface SPUUserUpdateState : NSObject - (instancetype)init NS_UNAVAILABLE; +/** + The current update stage. + + This stage indicates if data has been already downloaded or not, or if an update is currently being installed. + */ @property (nonatomic, readonly) SPUUserUpdateStage stage; + +/** + Indicates whether or not the update check was initiated by the user. + */ @property (nonatomic, readonly) BOOL userInitiated; @end diff --git a/Sparkle/SUAppcast.h b/Sparkle/SUAppcast.h index a5481d61..39d3f51e 100644 --- a/Sparkle/SUAppcast.h +++ b/Sparkle/SUAppcast.h @@ -22,8 +22,19 @@ NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem; + +/** + The appcast representing a collection of `SUAppcastItem` items in the feed. + */ SU_EXPORT @interface SUAppcast : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/** + The collection of update items. + + These `SUAppcastItem` items are in the same order as specified in the appcast XML feed and are thus not sorted by version. + */ @property (readonly, copy) NSArray *items; @end diff --git a/Sparkle/SUAppcastItem+Private.h b/Sparkle/SUAppcastItem+Private.h index 8d1c9f31..77536148 100644 --- a/Sparkle/SUAppcastItem+Private.h +++ b/Sparkle/SUAppcastItem+Private.h @@ -33,6 +33,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver failureReason:(NSString * _Nullable __autoreleasing *_Nullable)error; +/** + The DSA and EdDSA signatures along with their statuses. + */ +@property (readonly, nullable) SUSignatures *signatures; + @end NS_ASSUME_NONNULL_END diff --git a/Sparkle/SUAppcastItem.h b/Sparkle/SUAppcastItem.h index e0e3b205..4d79a763 100644 --- a/Sparkle/SUAppcastItem.h +++ b/Sparkle/SUAppcastItem.h @@ -33,41 +33,309 @@ NS_ASSUME_NONNULL_BEGIN -SU_EXPORT @interface SUAppcastItem : NSObject -@property (copy, readonly, nullable) NSString *title; -@property (copy, readonly, nullable) NSString *dateString; -@property (copy, readonly, nullable) NSDate *date; -@property (copy, readonly, nullable) NSString *itemDescription; -@property (strong, readonly, nullable) NSURL *releaseNotesURL; -@property (strong, readonly, nullable) SUSignatures *signatures; -@property (copy, readonly, nullable) NSString *minimumSystemVersion; -@property (copy, readonly, nullable) NSString *maximumSystemVersion; -@property (strong, readonly, nullable) NSURL *fileURL; -@property (nonatomic, readonly) uint64_t contentLength; -@property (copy, readonly) NSString *versionString; -@property (copy, readonly, nullable) NSString *osString; -@property (copy, readonly, nullable) NSString *displayVersionString; -@property (copy, readonly, nullable) NSDictionary *deltaUpdates; -@property (strong, readonly, nullable) NSURL *infoURL; -@property (copy, readonly, nullable) NSNumber* phasedRolloutInterval; -@property (nonatomic, copy, readonly) NSString *installationType; -@property (copy, readonly, nullable) NSString *minimumAutoupdateVersion; -@property (nonatomic, readonly, nullable) NSString *channel; +/** + The appcast item describing an update in the application's appcast feed. -@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; -@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; -@property (getter=isMajorUpgrade, readonly) BOOL majorUpgrade; -@property (getter=isMacOsUpdate, readonly) BOOL macOsUpdate; + An appcast item represents a single update item in the `SUAppcast` contained within the @c element. + + Every appcast item must have a `versionString`, and either a `fileURL` or an `infoURL`. + All the remaining properties describing an update to the application are optional. + + Extended documentation and examples on using appcast item features are available at: + https://sparkle-project.org/documentation/publishing/ + */ +SU_EXPORT @interface SUAppcastItem : NSObject + +/** + The version of the update item. + + Sparkle uses this property to compare update items and determine the best available update item in the `SUAppcast`. + + This corresponds to the application update's @c CFBundleVersion + + This is extracted from the @c element, or the @c sparkle:version attribute from the @c element. + */ +@property (copy, readonly) NSString *versionString; + +/** + The human-readable display version of the update item if provided. + + This is the version string shown to the user when they are notified of a new update. + + This corresponds to the application update's @c CFBundleShortVersionString + + This is extracted from the @c element, or the @c sparkle:shortVersionString attribute from the @c element. + */ +@property (copy, readonly, nullable) NSString *displayVersionString; + +/** + The file URL to the update item if provided. + + This download contains the actual update Sparkle will attempt to install. + In cases where a download cannot be provided, an `infoURL` must be provided instead. + + A file URL should have an accompanying `contentLength` provided. + + This is extracted from the @c url attribute in the @c element. + */ +@property (readonly, nullable) NSURL *fileURL; + +/** + The content length of the download in bytes. + + This property is used as a fallback when the server doesn't report the content length of the download. + In that case, it is used to report progress of the downloading update to the user. + + A warning is outputted if this property is not equal the server's expected content length (if provided). + + This is extracted from the @c length attribute in the @c element. + It should be specified if a `fileURL` is provided. + */ +@property (nonatomic, readonly) uint64_t contentLength; + +/** + The info URL to the update item if provided. + + This informational link is used to direct the user to learn more about an update they cannot download/install directly from within the application. + The link should point to the product's web page. + + The informational link will be used if `informationOnlyUpdate` is @c YES + + This is extracted from the @c element. + */ +@property (readonly, nullable) NSURL *infoURL; + +/** + Indicates whether or not the update item is only informational and has no download. + + If `infoURL` is not present, this is @c NO + + If `fileURL` is not present, this is @c YES + + Otherwise this is determined based on the contents extracted from the @c element. + */ @property (getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate; +/** + The title of the appcast item if provided. + + This is extracted from the @c element. + */ +@property (copy, readonly, nullable) NSString *title; + +/** + The date string of the appcast item if provided. + + The `date` property is constructed from this property and expects this string to comply with the following date format: + `E, dd MMM yyyy HH:mm:ss Z` + + This is extracted from the @c <pubDate> element. + */ +@property (copy, readonly, nullable) NSString *dateString; + +/** + The date constructed from the `dateString` property if provided. + + Sparkle by itself only uses this property for phased group rollouts specified via `phasedRolloutInterval`, but clients may query this property too. + + This date is constructed using the @c en_US locale. + */ +@property (copy, readonly, nullable) NSDate *date; + +/** + The release notes URL of the appcast item if provided. + + This external link points to an HTML file that Sparkle downloads and renders to show the user a new or old update item's changelog. + + An alternative to using an external release notes link is providing an embedded `itemDescription`. + + This is extracted from the @c <sparkle:releaseNotesLink> element. + */ +@property (readonly, nullable) NSURL *releaseNotesURL; + +/** + The description of the appcast item if provided. + + A description may be provided for inline/embedded release notes for new updates using @c <![CDATA[...]]> + This is an alternative to providing a `releaseNotesURL`. + + This is extracted from the @c <description> element. + */ +@property (copy, readonly, nullable) NSString *itemDescription; + +/** + The required minimum system operating version string for this update if provided. + + This version string should contain three period-separated components. + + Example: @c 10.12.0 + + Use `minimumOperatingSystemVersionIsOK` property to test if the current running system passes this requirement. + + This is extracted from the @c <sparkle:minimumSystemVersion> element. + */ +@property (copy, readonly, nullable) NSString *minimumSystemVersion; + +/** + Indicates whether or not the current running system passes the `minimumSystemVersion` requirement. + */ @property (nonatomic, readonly) BOOL minimumOperatingSystemVersionIsOK; + +/** + The required maximum system operating version string for this update if provided. + + A maximum system operating version requirement should only be made in unusual scenarios. + + This version string should contain three period-separated components. + + Example: @c 10.13.0 + + Use `maximumOperatingSystemVersionIsOK` property to test if the current running system passes this requirement. + + This is extracted from the @c <sparkle:maximumSystemVersion> element. + */ +@property (copy, readonly, nullable) NSString *maximumSystemVersion; + +/** + Indicates whether or not the current running system passes the `maximumSystemVersion` requirement. + */ @property (nonatomic, readonly) BOOL maximumOperatingSystemVersionIsOK; -// Returns the dictionary representing the appcast item; this might be useful later for extensions. +/** + The channel the update item is on if provided. + + An update item may specify a custom channel name (such as @c beta) that can only be found by updaters that filter for that channel. + If no channel is provided, the update item is assumed to be on the default channel. + + This is extracted from the @c <sparkle:channel> element. + Old applications must be using Sparkle 2 or later to interpret the channel element and to ignore unmatched channels. + */ +@property (nonatomic, readonly, nullable) NSString *channel; + +/** + The installation type of the update at `fileURL` + + This may be: + - @c application - indicates this is a regular application update. + - @c package - indicates this is a guided package installer update. + - @c interactive-package - indicates this is an interactive package installer update (deprecated; use "package" instead) + + This is extracted from the @c sparkle:installationType attribute in the @c <enclosure> element. + + If no installation type is provided in the enclosure, the installation type is inferred from the `fileURL` file extension instead. + + If the file extension is @c pkg or @c mpkg, the installation type is @c package otherwise it is @c application + + Hence, the installation type in the enclosure element only needs to be specified for package based updates distributed inside of a @c zip or other archive format. + + Old applications must be using Sparkle 1.26 or later to support downloading bare package updates (`pkg` or `mpkg`) that are not additionally archived inside of a @c zip or other archive format. + */ +@property (nonatomic, copy, readonly) NSString *installationType; + +/** + The phased rollout interval of the update item in seconds if provided. + + This is the interval between when different groups of users are notified of a new update. + + For this property to be used by Sparkle, the published `date` on the update item must be present as well. + + After each interval after the update item's `date`, a new group of users become eligible for being notified of the new update. + + This is extracted from the @c <sparkle:phasedRolloutInterval> element. + + Old applications must be using Sparkle 1.25 or later to support phased rollout intervals, otherwise they may assume updates are immediately available. + */ +@property (copy, readonly, nullable) NSNumber* phasedRolloutInterval; + +/** + The minimum bundle version string this update requires for automatically downloading and installing updates if provided. + + If an application's bundle version meets this version requirement, it can install the new update item in the background automatically. + + Otherwise if the requirement is not met, the user is always prompted to install the update. In this case, the update is assumed to be a `majorUpgrade`. + + If the update is a `majorUpgrade` and the update is skipped by the user, other future update alerts with the same `minimumAutoupdateVersion` will also be skipped. + + This version string corresponds to the application's @c CFBundleVersion + */ +@property (copy, readonly, nullable) NSString *minimumAutoupdateVersion; + +/** + Indicates whether or not the update item is a major upgrade. + + An update is a major upgrade if the application's bundle version doesn't meet the `minimumAutoupdateVersion` requirement. + */ +@property (getter=isMajorUpgrade, readonly) BOOL majorUpgrade; + +/** + Indicates whether or not the update item is critical. + + Critical updates are shown to the user more promptly. Sparkle's standard user interface also does not allow them to be skipped. + + This is determined and extracted from a top-level @c <sparkle:criticalUpdate> element or a @c sparkle:criticalUpdate element inside of a @c sparkle:tags element. + + Old applications must be using Sparkle 2 or later to support the top-level @c <sparkle:criticalUpdate> element. + */ +@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; + +/** + Specifies the operating system the download update is available for if provided. + + If this property is not provided, then the supported operating system is assumed to be macOS. + + Known potential values for this string are @c macos and @c windows + + Sparkle on Mac ignores update items that are for other operating systems. + This is only useful for sharing appcasts between Sparkle on Mac and Sparkle on other operating systems. + + Use `macOsUpdate` property to test if this update item is for macOS. + + This is extracted from the @c sparkle:os attribute in the @c <enclosure> element. + */ +@property (copy, readonly, nullable) NSString *osString; + +/** + Indicates whether or not this update item is for macOS. + + This is determined from the `osString` property. + */ +@property (getter=isMacOsUpdate, readonly) BOOL macOsUpdate; + +/** + The delta updates for this update item. + + Sparkle uses these to download and apply a smaller update based on the version the user is updating from. + + The key is based on the @c sparkle:version of the update. + The value is an update item that will have `deltaUpdate` be @c YES + + Clients typically should not need to examine the contents of the delta updates. + + This is extracted from the @c <sparkle:deltas> element. + */ +@property (copy, readonly, nullable) NSDictionary<NSString *, SUAppcastItem *> *deltaUpdates; + +/** + Indicates whether or not the update item is a delta update. + + An update item is a delta update if it is in the `deltaUpdates` of another update item. + */ +@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; + +/** + The dictionary representing the entire appcast item. + + This is useful for querying custom extensions or elements from the appcast item. + */ @property (readonly, copy) NSDictionary *propertiesDictionary; - (instancetype)init NS_UNAVAILABLE; +/** + An empty appcast item. + + This may be used as a potential return value in `-[SPUUpdaterDelegate bestValidUpdateInAppcast:forUpdater:]` + */ + (instancetype)emptyAppcastItem; // Deprecated initializers diff --git a/Sparkle/SUAppcastItem.m b/Sparkle/SUAppcastItem.m index fdb427d6..11cd0c88 100644 --- a/Sparkle/SUAppcastItem.m +++ b/Sparkle/SUAppcastItem.m @@ -37,6 +37,8 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; @interface SUAppcastItem () +@property (readonly, nullable) SUSignatures *signatures; + // Auxillary appcast item state that needs to be evaluated based on the host state // This may be nil if the client creates an SUAppcastItem with a deprecated initializer // In that case we will need to fallback to safe behavior @@ -386,14 +388,14 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; // Need an info URL or an enclosure URL. Former to show "More Info" // page, latter to download & install: - if (!enclosure && !theInfoURL) { + if (!enclosure && !_infoURL) { if (error) { *error = @"No enclosure in feed item"; } return nil; } - if (theInfoURL != nil) { + if (_infoURL != nil) { // If enclosure doesn't exist, the update must be an informational update // Otherwise check presence of informational update element _informationalUpdateVersions = (enclosure != nil) ? [dict objectForKey:SUAppcastElementInformationalUpdate] : [NSSet set]; @@ -403,7 +405,7 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; } NSString *enclosureURLString = [enclosure objectForKey:SURSSAttributeURL]; - if (!enclosureURLString && !theInfoURL) { + if (!enclosureURLString && !_infoURL) { if (error) { *error = @"Feed item's enclosure lacks URL"; } diff --git a/Sparkle/SUErrors.h b/Sparkle/SUErrors.h index 058fc927..04ac88f7 100644 --- a/Sparkle/SUErrors.h +++ b/Sparkle/SUErrors.h @@ -78,11 +78,29 @@ typedef NS_ENUM(OSStatus, SUError) { SUIncorrectAPIUsageError = 5000 }; +/** + The reason why a new update is not available. + */ typedef NS_ENUM(OSStatus, SPUNoUpdateFoundReason) { + /** + A new update is unavailable for an unknown reason. + */ SPUNoUpdateFoundReasonUnknown, + /** + A new update is unavailable because the user is on the latest known version in the appcast feed. + */ SPUNoUpdateFoundReasonOnLatestVersion, + /** + A new update is unavailable because the user is on a version newer than the latest known version in the appcast feed. + */ SPUNoUpdateFoundReasonOnNewerThanLatestVersion, + /** + A new update is unavailable because the user's operating system version is too old for the update. + */ SPUNoUpdateFoundReasonSystemIsTooOld, + /** + A new update is unavailable because the user's operating system version is too new for the update. + */ SPUNoUpdateFoundReasonSystemIsTooNew }; diff --git a/Sparkle/SUStandardVersionComparator.h b/Sparkle/SUStandardVersionComparator.h index 3df3d462..1665ed40 100644 --- a/Sparkle/SUStandardVersionComparator.h +++ b/Sparkle/SUStandardVersionComparator.h @@ -47,18 +47,23 @@ SU_EXPORT @interface SUStandardVersionComparator : NSObject <SUVersionComparison - (instancetype)init; /** - Returns a singleton instance of the comparator. - - It is usually preferred to alloc/init new a comparator instead. + A singleton instance of the comparator. */ -+ (SUStandardVersionComparator *)defaultComparator; +@property (nonatomic, class, readonly) SUStandardVersionComparator *defaultComparator; /** - Compares version strings through textual analysis. - - See the implementation for more details. + Compares two version strings through textual analysis. + + These version strings should be in the format of x, x.y, or x.y.z where each component is a number. + For example, valid version strings include "1.5.3", "500", or "4000.1" + These versions that are compared correspond to the @c CFBundleVersion values of the updates. + + @param versionA The first version string to compare. + @param versionB The second version string to compare. + @return A comparison result between @c versionA and @c versionB */ - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SUAppcastTest.swift b/Tests/SUAppcastTest.swift index 2d27a3c1..a4feefab 100644 --- a/Tests/SUAppcastTest.swift +++ b/Tests/SUAppcastTest.swift @@ -17,7 +17,7 @@ class SUAppcastTest: XCTestCase { do { let testData = try Data(contentsOf: testURL) - let versionComparator = SUStandardVersionComparator.default() + let versionComparator = SUStandardVersionComparator.default let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) @@ -86,7 +86,7 @@ class SUAppcastTest: XCTestCase { do { let testData = try Data(contentsOf: testURL) - let versionComparator = SUStandardVersionComparator.default() + let versionComparator = SUStandardVersionComparator.default let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) @@ -144,7 +144,7 @@ class SUAppcastTest: XCTestCase { do { let testData = try Data(contentsOf: testURL) - let versionComparator = SUStandardVersionComparator.default() + let versionComparator = SUStandardVersionComparator.default // If critical update version is 1.5 and host version is 1.0, update should be marked critical do { @@ -184,7 +184,7 @@ class SUAppcastTest: XCTestCase { do { let testData = try Data(contentsOf: testURL) - let versionComparator = SUStandardVersionComparator.default() + let versionComparator = SUStandardVersionComparator.default // Test informational updates from version 1.0 do { @@ -200,7 +200,7 @@ class SUAppcastTest: XCTestCase { // Test delta updates inheriting informational only updates do { - let deltaUpdate = appcast.items[2].deltaUpdates!["2.0"] as! SUAppcastItem + let deltaUpdate = appcast.items[2].deltaUpdates!["2.0"]! XCTAssertTrue(deltaUpdate.isInformationOnlyUpdate) } } @@ -612,7 +612,7 @@ class SUAppcastTest: XCTestCase { do { let testFileData = try Data(contentsOf: testFileUrl) - let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default(), standardVersionComparator: SUStandardVersionComparator.default()) + let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testFileData, relativeTo: testFileUrl, stateResolver: stateResolver) let items = appcast.items @@ -628,7 +628,7 @@ class SUAppcastTest: XCTestCase { let testData = NSData(contentsOfFile: testFile)! do { - let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default(), standardVersionComparator: SUStandardVersionComparator.default()) + let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: nil, stateResolver: stateResolver) let items = appcast.items @@ -652,7 +652,7 @@ class SUAppcastTest: XCTestCase { do { let baseURL = URL(string: "https://fake.sparkle-project.org/updates/index.xml")! - let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default(), standardVersionComparator: SUStandardVersionComparator.default()) + let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver) let items = appcast.items diff --git a/sparkle-cli/SPUCommandLineDriver.m b/sparkle-cli/SPUCommandLineDriver.m index 250fb863..5fc02d49 100644 --- a/sparkle-cli/SPUCommandLineDriver.m +++ b/sparkle-cli/SPUCommandLineDriver.m @@ -71,7 +71,7 @@ exit(EXIT_SUCCESS); } -- (void)updaterWillIdleSchedulingUpdates:(SPUUpdater *)__unused updater __attribute__((noreturn)) +- (void)updaterWillNotScheduleUpdateCheck:(SPUUpdater *)__unused updater __attribute__((noreturn)) { if (self.verbose) { fprintf(stderr, "Automatic update checks are disabled. Exiting.\n");