diff --git a/Documentation.zip b/Documentation.zip new file mode 100644 index 000000000..6bcb5d0cf Binary files /dev/null and b/Documentation.zip differ diff --git a/Info.plist b/Info.plist new file mode 100644 index 000000000..96ac2ab5a --- /dev/null +++ b/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleName + ${PRODUCT_NAME} + CFBundleIconFile + + CFBundleIdentifier + org.andymatuschak.Sparkle + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + 1.1 + NSPrincipalClass + + + diff --git a/License.txt b/License.txt new file mode 100644 index 000000000..20466c417 --- /dev/null +++ b/License.txt @@ -0,0 +1,7 @@ +Copyright (c) 2006 Andy Matuschak + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a40da763e --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: all localizable-strings + +localizable-strings: + rm English.lproj/Sparkle.strings || TRUE + genstrings -o English.lproj -s SULocalizedString *.m *.h + mv English.lproj/Localizable.strings English.lproj/Sparkle.strings + diff --git a/NSApplication+AppCopies.h b/NSApplication+AppCopies.h new file mode 100644 index 000000000..ee901e685 --- /dev/null +++ b/NSApplication+AppCopies.h @@ -0,0 +1,13 @@ +// +// NSApplication+AppCopies.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +@interface NSApplication (SUAppCopies) +- (int)copiesRunning; +@end diff --git a/NSApplication+AppCopies.m b/NSApplication+AppCopies.m new file mode 100644 index 000000000..0bba7e269 --- /dev/null +++ b/NSApplication+AppCopies.m @@ -0,0 +1,27 @@ +// +// NSApplication+AppCopies.m +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "NSApplication+AppCopies.h" +#import "SUUtilities.h" + +@implementation NSApplication (SUAppCopies) + +- (int)copiesRunning +{ + id appEnumerator = [[[NSWorkspace sharedWorkspace] launchedApplications] objectEnumerator], currentApp; + int count = 0; + while ((currentApp = [appEnumerator nextObject])) + { + // Potential gotcha: the new version of your app better have the same NSApplicationName. + if([[currentApp objectForKey:@"NSApplicationName"] isEqualToString:SUHostAppName()]) + count++; + } + return count; +} + +@end diff --git a/NSFileManager+Authentication.h b/NSFileManager+Authentication.h new file mode 100644 index 000000000..c995911ca --- /dev/null +++ b/NSFileManager+Authentication.h @@ -0,0 +1,11 @@ +// +// NSFileManager+Authentication.m +// Sparkle +// +// Created by Andy Matuschak on 3/9/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +@interface NSFileManager (SUAuthenticationAdditions) +- (BOOL)movePathWithAuthentication:(NSString *)src toPath:(NSString *)dst; +@end diff --git a/NSFileManager+Authentication.m b/NSFileManager+Authentication.m new file mode 100644 index 000000000..e6f8029f6 --- /dev/null +++ b/NSFileManager+Authentication.m @@ -0,0 +1,109 @@ +// +// NSFileManager+Authentication.m +// Sparkle +// +// Created by Andy Matuschak on 3/9/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +// This code based on generous contribution from Allan Odgaard. Thanks, Allan! + +#import "sys/stat.h" +#import + +#import +#import +#import + +@implementation NSFileManager (SUAuthenticationAdditions) + +- (BOOL)currentUserOwnsPath:(NSString *)oPath +{ + char *path = (char *)[oPath fileSystemRepresentation]; + unsigned int uid = getuid(); + bool res = false; + struct stat sb; + if(stat(path, &sb) == 0) + { + if(sb.st_uid == uid) + { + res = true; + if(sb.st_mode & S_IFDIR) + { + DIR* dir = opendir(path); + struct dirent* entry = NULL; + while(res && (entry = readdir(dir))) + { + if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + char descend[strlen(path) + 1 + entry->d_namlen + 1]; + strcpy(descend, path); + strcat(descend, "/"); + strcat(descend, entry->d_name); + res = [self currentUserOwnsPath:[NSString stringWithUTF8String:descend]]; + } + closedir(dir); + } + } + } + return res; +} + +- (BOOL)_movePathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst +{ + NSString *tmp = [[[dst stringByDeletingPathExtension] stringByAppendingString:@".old"] stringByAppendingPathExtension:[dst pathExtension]]; + BOOL res = NO; + struct stat sb; + if((stat([src UTF8String], &sb) != 0) || (stat([tmp UTF8String], &sb) == 0) || stat([dst UTF8String], &sb) != 0) + return false; + + char* buf = NULL; + asprintf(&buf, + "mv -f \"$DST_PATH\" \"$TMP_PATH\" && " + "mv -f \"$SRC_PATH\" \"$DST_PATH\" && " + "rm -rf \"$TMP_PATH\" && " + "chown -R %d:%d \"$DST_PATH\"", + sb.st_uid, sb.st_gid); + + if(!buf) + return false; + + AuthorizationRef auth; + if(AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth) == errAuthorizationSuccess) + { + setenv("SRC_PATH", [src UTF8String], 1); + setenv("DST_PATH", [dst UTF8String], 1); + setenv("TMP_PATH", [tmp UTF8String], 1); + sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); + char const* arguments[] = { "-c", buf, NULL }; + if(AuthorizationExecuteWithPrivileges(auth, "/bin/sh", kAuthorizationFlagDefaults, (char**)arguments, NULL) == errAuthorizationSuccess) + { + int status; + int pid = wait(&status); + if(pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0) + res = YES; + } + signal(SIGCHLD, oldSigChildHandler); + } + AuthorizationFree(auth, 0); + free(buf); + return res; +} + +- (BOOL)movePathWithAuthentication:(NSString *)src toPath:(NSString *)dst +{ + if ([[NSFileManager defaultManager] isWritableFileAtPath:dst] && [[NSFileManager defaultManager] isWritableFileAtPath:[dst stringByDeletingLastPathComponent]]) + { + int tag = 0; + BOOL result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[dst stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[dst lastPathComponent]] tag:&tag]; + result &= [[NSFileManager defaultManager] movePath:src toPath:dst handler:NULL]; + return result; + } + else + { + return [self _movePathWithForcedAuthentication:src toPath:dst]; + } +} + +@end diff --git a/NSFileManager+Verification.h b/NSFileManager+Verification.h new file mode 100644 index 000000000..f0ce7c20c --- /dev/null +++ b/NSFileManager+Verification.h @@ -0,0 +1,15 @@ +// +// NSFileManager+Verification.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +// For the paranoid folks! +@interface NSFileManager (SUVerification) +- (BOOL)validatePath:(NSString *)path withMD5Hash:(NSString *)hash; +- (BOOL)validatePath:(NSString *)path withEncodedDSASignature:(NSString *)encodedSignature; +@end diff --git a/NSFileManager+Verification.m b/NSFileManager+Verification.m new file mode 100644 index 000000000..c0ddb959e --- /dev/null +++ b/NSFileManager+Verification.m @@ -0,0 +1,153 @@ +// +// NSFileManager+Verification.m +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +// DSA stuff adapted from code provided by Allan Odgaard. Thanks, Allan! + +#import "NSFileManager+Verification.h" +#import "SUUtilities.h" +#import "md5.h" + +#import +#import +#import +#import +#import +#import + +int b64decode(unsigned char* str) +{ + unsigned char *cur, *start; + int d, dlast, phase; + unsigned char c; + static int table[256] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 00-0F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 10-1F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, /* 20-2F */ + 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, /* 30-3F */ + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, /* 40-4F */ + 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, /* 50-5F */ + -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, /* 60-6F */ + 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, /* 70-7F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 80-8F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 90-9F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* A0-AF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* B0-BF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* C0-CF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* D0-DF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* E0-EF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 /* F0-FF */ + }; + + d = dlast = phase = 0; + start = str; + for (cur = str; *cur != '\0'; ++cur ) + { + if(*cur == '\n' || *cur == '\r'){phase = dlast = 0; continue;} + d = table[(int)*cur]; + if(d != -1) + { + switch(phase) + { + case 0: + ++phase; + break; + case 1: + c = ((dlast << 2) | ((d & 0x30) >> 4)); + *str++ = c; + ++phase; + break; + case 2: + c = (((dlast & 0xf) << 4) | ((d & 0x3c) >> 2)); + *str++ = c; + ++phase; + break; + case 3: + c = (((dlast & 0x03 ) << 6) | d); + *str++ = c; + phase = 0; + break; + } + dlast = d; + } + } + *str = '\0'; + return str - start; +} + +EVP_PKEY* load_dsa_key(char *key) +{ + EVP_PKEY* pkey = NULL; + BIO *bio; + if((bio = BIO_new_mem_buf(key, strlen(key)))) + { + DSA* dsa_key = 0; + if(PEM_read_bio_DSA_PUBKEY(bio, &dsa_key, NULL, NULL)) + { + if((pkey = EVP_PKEY_new())) + { + if(EVP_PKEY_assign_DSA(pkey, dsa_key) != 1) + { + DSA_free(dsa_key); + EVP_PKEY_free(pkey); + pkey = NULL; + } + } + } + BIO_free(bio); + } + return pkey; +} + +@implementation NSFileManager (SUVerification) + +- (BOOL)validatePath:(NSString *)path withMD5Hash:(NSString *)hash +{ + NSData *data = [NSData dataWithContentsOfFile:path]; + if (!data) { return NO; } + + md5_state_t md5_state; + md5_init(&md5_state); + md5_append(&md5_state, [data bytes], [data length]); + unsigned char digest[16]; + md5_finish(&md5_state, digest); + + int di; + char hexDigest[32]; + for (di = 0; di < 16; di++) + sprintf(hexDigest + di*2, "%02x", digest[di]); + + return [hash isEqualToString:[NSString stringWithCString:hexDigest]]; +} + +- (BOOL)validatePath:(NSString *)path withEncodedDSASignature:(NSString *)encodedSignature +{ + EVP_PKEY* pkey; + if(!encodedSignature || !SUInfoValueForKey(SUPublicDSAKeyKey) || !(pkey = load_dsa_key((char *)[SUInfoValueForKey(SUPublicDSAKeyKey) UTF8String]))) + return NO; + + // Now, the signature is in base64; we have to decode it into a binary stream. + unsigned char *signature = (unsigned char *)[encodedSignature UTF8String]; + long length = b64decode(signature); + + NSData *pathData = [NSData dataWithContentsOfFile:path]; + if (!pathData) { return NO; } + unsigned char md[SHA_DIGEST_LENGTH]; + SHA1([pathData bytes], [pathData length], md); + + BOOL res = false; + EVP_MD_CTX ctx; + if(EVP_VerifyInit(&ctx, EVP_dss1()) == 1) + { + EVP_VerifyUpdate(&ctx, md, SHA_DIGEST_LENGTH); + res = EVP_VerifyFinal(&ctx, signature, length, pkey) == 1; + } + EVP_PKEY_free(pkey); + return res; +} + +@end diff --git a/NSString+extras.h b/NSString+extras.h new file mode 100644 index 000000000..498e4d01c --- /dev/null +++ b/NSString+extras.h @@ -0,0 +1,61 @@ +/* + +BSD License + +Copyright (c) 2002, Brent Simmons +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of ranchero.com or Brent Simmons nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +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. + + +*/ + + +/* + NSString+extras.h + NetNewsWire + + Created by Brent Simmons on Fri Jun 14 2002. + Copyright (c) 2002 Brent Simmons. All rights reserved. +*/ + + +#import +#import + + +@interface NSString (extras) + +- (NSString *)stringWithSubstitute:(NSString *)subs forCharactersFromSet:(NSCharacterSet *)set; + +- (NSString *) trimWhiteSpace; + +- (NSString *) stripHTML; + +- (NSString *) ellipsizeAfterNWords: (int) n; + ++ (BOOL) stringIsEmpty: (NSString *) s; + + +@end diff --git a/NSString+extras.m b/NSString+extras.m new file mode 100644 index 000000000..b5bfb4c6f --- /dev/null +++ b/NSString+extras.m @@ -0,0 +1,135 @@ +/* + +BSD License + +Copyright (c) 2002, Brent Simmons +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of ranchero.com or Brent Simmons nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +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. + + +*/ + + +/* + NSString+extras.m + NetNewsWire + + Created by Brent Simmons on Fri Jun 14 2002. + Copyright (c) 2002 Brent Simmons. All rights reserved. +*/ + +#import "NSString+extras.h" + + +@implementation NSString (extras) + +- (NSString *)stringWithSubstitute:(NSString *)subs forCharactersFromSet:(NSCharacterSet *)set +{ + NSRange r = [self rangeOfCharacterFromSet:set]; + if (r.location == NSNotFound) return self; + NSMutableString *newString = [self mutableCopy]; + do + { + [newString replaceCharactersInRange:r withString:subs]; + r = [newString rangeOfCharacterFromSet:set]; + } + while (r.location != NSNotFound); + return [newString autorelease]; +} + +- (NSString *) trimWhiteSpace { + + NSMutableString *s = [[self mutableCopy] autorelease]; + + CFStringTrimWhitespace ((CFMutableStringRef) s); + + return (NSString *) [[s copy] autorelease]; + } /*trimWhiteSpace*/ + + +- (NSString *) ellipsizeAfterNWords: (int) n { + + NSArray *stringComponents = [self componentsSeparatedByString: @" "]; + NSMutableArray *componentsCopy = [stringComponents mutableCopy]; + int ix = n; + int len = [componentsCopy count]; + + if (len < n) + ix = len; + + [componentsCopy removeObjectsInRange: NSMakeRange (ix, len - ix)]; + + return [componentsCopy componentsJoinedByString: @" "]; + } /*ellipsizeAfterNWords*/ + + +- (NSString *) stripHTML { + + int len = [self length]; + NSMutableString *s = [NSMutableString stringWithCapacity: len]; + int i = 0, level = 0; + + for (i = 0; i < len; i++) { + + NSString *ch = [self substringWithRange: NSMakeRange (i, 1)]; + + if ([ch isEqualTo: @"<"]) + level++; + + else if ([ch isEqualTo: @">"]) { + + level--; + + if (level == 0) + [s appendString: @" "]; + } /*else if*/ + + else if (level == 0) + [s appendString: ch]; + } /*for*/ + + return (NSString *) [[s copy] autorelease]; + } /*stripHTML*/ + + ++ (BOOL) stringIsEmpty: (NSString *) s { + + NSString *copy; + + if (s == nil) + return (YES); + + if ([s isEqualTo: @""]) + return (YES); + + copy = [[s copy] autorelease]; + + if ([[copy trimWhiteSpace] isEqualTo: @""]) + return (YES); + + return (NO); + } /*stringIsEmpty*/ + +@end diff --git a/RSS.h b/RSS.h new file mode 100644 index 000000000..82da04a44 --- /dev/null +++ b/RSS.h @@ -0,0 +1,98 @@ +/* + +BSD License + +Copyright (c) 2002, Brent Simmons +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of ranchero.com or Brent Simmons nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +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. + + +*/ + +/* + RSS.h + A class for reading RSS feeds. + + Created by Brent Simmons on Wed Apr 17 2002. + Copyright (c) 2002 Brent Simmons. All rights reserved. +*/ + + +#import +#import +#import "NSString+extras.h" + + +@interface RSS : NSObject { + + NSDictionary *headerItems; + NSMutableArray *newsItems; + NSString *version; + + BOOL flRdf; + BOOL normalize; + } + + +/*Public*/ + +- (RSS *) initWithTitle: (NSString *) title andDescription: (NSString *) description; + +- (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl; + +- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl; +- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent:(NSString *)userAgent; + +- (NSDictionary *) headerItems; + +- (NSMutableArray *) newsItems; + +- (NSString *) version; + +// AMM's extensions for Sparkle +- (NSDictionary *)newestItem; + + +/*Private*/ + +- (void) createheaderdictionary: (CFXMLTreeRef) tree; + +- (void) createitemsarray: (CFXMLTreeRef) tree; + +- (void) setversionstring: (CFXMLTreeRef) tree; + +- (void) flattenimagechildren: (CFXMLTreeRef) tree into: (NSMutableDictionary *) dictionary; + +- (void) flattensourceattributes: (CFXMLNodeRef) node into: (NSMutableDictionary *) dictionary; + +- (CFXMLTreeRef) getchanneltree: (CFXMLTreeRef) tree; + +- (CFXMLTreeRef) getnamedtree: (CFXMLTreeRef) currentTree name: (NSString *) name; + +- (void) normalizeRSSItem: (NSMutableDictionary *) rssItem; + +- (NSString *) getelementvalue: (CFXMLTreeRef) tree; + +@end diff --git a/RSS.m b/RSS.m new file mode 100644 index 000000000..164c16de9 --- /dev/null +++ b/RSS.m @@ -0,0 +1,690 @@ +/* + +BSD License + +Copyright (c) 2002, Brent Simmons +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of ranchero.com or Brent Simmons nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +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. + + +*/ + +/* + RSS.m + A class for reading RSS feeds. + + Created by Brent Simmons on Wed Apr 17 2002. + Copyright (c) 2002 Brent Simmons. All rights reserved. +*/ + + +#import "RSS.h" + +// This comparator function is used to sort the RSS items by their published date. +int compareNewsItems(id item1, id item2, void *context) +{ + // We compare item2 with item1 instead of the other way 'round because we want descending, not ascending. Bit of a hack. + return [(NSDate *)[NSDate dateWithNaturalLanguageString:[item2 objectForKey:@"pubDate"]] compare:(NSDate *)[NSDate dateWithNaturalLanguageString:[item1 objectForKey:@"pubDate"]]]; +} + +@implementation RSS + + +#define titleKey @"title" +#define linkKey @"link" +#define descriptionKey @"description" + + +/*Public interface*/ + +- (NSDictionary *)newestItem +{ + // The news items are already sorted by published date, descending. + return [[self newsItems] objectAtIndex:0]; +} + +- (RSS *) initWithTitle: (NSString *) title andDescription: (NSString *) description { + + /* + Create an empty feed. Useful for synthetic feeds. + */ + + NSMutableDictionary *header; + + flRdf = NO; + + header = [NSMutableDictionary dictionaryWithCapacity: 2]; + + [header setObject: title forKey: titleKey]; + + [header setObject: description forKey: descriptionKey]; + + headerItems = (NSDictionary *) [header copy]; + + newsItems = [[NSMutableArray alloc] initWithCapacity: 0]; + + version = [[NSString alloc] initWithString: @"synthetic"]; + + return (self); + } /*initWithTitle*/ + + +- (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl { + + CFXMLTreeRef tree; + + flRdf = NO; + + normalize = fl; + + NS_DURING + + tree = CFXMLTreeCreateFromData (kCFAllocatorDefault, (CFDataRef) rssData, + NULL, kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion); + + NS_HANDLER + + tree = nil; + + NS_ENDHANDLER + + if (tree == nil) { + + /*If there was a problem parsing the RSS file, + raise an exception.*/ + + [self release]; + return nil; + } /*if*/ + + [self createheaderdictionary: tree]; + + [self createitemsarray: tree]; + + [self setversionstring: tree]; + + CFRelease (tree); + + return (self); + } /*initWithData*/ + + +- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl +{ + return [self initWithURL: url normalize: fl userAgent: nil]; +} + + + +- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent: (NSString*)userAgent +{ + NSData *rssData; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url cachePolicy: NSURLRequestReloadIgnoringCacheData + timeoutInterval: 30.0]; + if (userAgent) + [request setValue: userAgent forHTTPHeaderField: @"User-Agent"]; + + NSURLResponse *response=0; + NSError *error=0; + + rssData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error]; + + if (rssData == nil) + { + NSException *exception = [NSException exceptionWithName: @"RSSDownloadFailed" + reason: [error localizedFailureReason] userInfo: [error userInfo] ]; + [exception raise]; + } + + return [self initWithData: rssData normalize: fl]; +} /*initWithUrl*/ + + +- (NSDictionary *) headerItems { + + return (headerItems); + } /*headerItems*/ + + +- (NSMutableArray *) newsItems { + + return (newsItems); + } /*newsItems*/ + + +- (NSString *) version { + + return (version); + } /*version*/ + + +- (void) dealloc { + + [headerItems release]; + + [newsItems release]; + + [version release]; + [super dealloc]; + } /*dealloc*/ + + + +/*Private methods. Don't call these: they may change.*/ + + +- (void) createheaderdictionary: (CFXMLTreeRef) tree { + + CFXMLTreeRef channelTree, childTree; + CFXMLNodeRef childNode; + int childCount, i; + NSString *childName; + NSMutableDictionary *headerItemsMutable; + + channelTree = [self getchanneltree: tree]; + + if (channelTree == nil) { + + NSException *exception = [NSException exceptionWithName: @"RSSCreateHeaderDictionaryFailed" + reason: @"Couldn't find the channel tree." userInfo: nil]; + + [exception raise]; + } /*if*/ + + childCount = CFTreeGetChildCount (channelTree); + + headerItemsMutable = [NSMutableDictionary dictionaryWithCapacity: childCount]; + + for (i = 0; i < childCount; i++) { + + childTree = CFTreeGetChildAtIndex (channelTree, i); + + childNode = CFXMLTreeGetNode (childTree); + + childName = (NSString *) CFXMLNodeGetString (childNode); + + if ([childName hasPrefix: @"rss:"]) + childName = [childName substringFromIndex: 4]; + + if ([childName isEqualToString: @"item"]) + break; + + if ([childName isEqualTo: @"image"]) + [self flattenimagechildren: childTree into: headerItemsMutable]; + + [headerItemsMutable setObject: [self getelementvalue: childTree] forKey: childName]; + } /*for*/ + + headerItems = [headerItemsMutable copy]; + } /*initheaderdictionary*/ + + +- (void) createitemsarray: (CFXMLTreeRef) tree { + + CFXMLTreeRef channelTree, childTree, itemTree; + CFXMLNodeRef childNode, itemNode; + NSString *childName; + NSString *itemName, *itemValue; + int childCount, itemChildCount, i, j; + NSMutableDictionary *itemDictionaryMutable; + NSMutableArray *itemsArrayMutable; + + if (flRdf) + channelTree = [self getnamedtree: tree name: @"rdf:RDF"]; + else + channelTree = [self getchanneltree: tree]; + + if (channelTree == nil) { + + NSException *exception = [NSException exceptionWithName: @"RSSCreateItemsArrayFailed" + reason: @"Couldn't find the news items." userInfo: nil]; + + [exception raise]; + } /*if*/ + + childCount = CFTreeGetChildCount (channelTree); + + itemsArrayMutable = [NSMutableArray arrayWithCapacity: childCount]; + + for (i = 0; i < childCount; i++) { + + childTree = CFTreeGetChildAtIndex (channelTree, i); + + childNode = CFXMLTreeGetNode (childTree); + + childName = (NSString *) CFXMLNodeGetString (childNode); + + if ([childName hasPrefix: @"rss:"]) + childName = [childName substringFromIndex: 4]; + + if (![childName isEqualToString: @"item"]) + continue; + + itemChildCount = CFTreeGetChildCount (childTree); + + itemDictionaryMutable = [NSMutableDictionary dictionaryWithCapacity: itemChildCount]; + + for (j = 0; j < itemChildCount; j++) { + + itemTree = CFTreeGetChildAtIndex (childTree, j); + + itemNode = CFXMLTreeGetNode (itemTree); + + itemName = (NSString *) CFXMLNodeGetString (itemNode); + + if ([itemName hasPrefix: @"rss:"]) + itemName = [itemName substringFromIndex: 4]; + + if ([itemName isEqualTo:@"enclosure"]) + { + // Hack to add attributes to the dictionary in addition to children. (AMM) + const CFXMLElementInfo *websiteInfo = CFXMLNodeGetInfoPtr(itemNode); + NSMutableDictionary *enclosureDictionary = [NSMutableDictionary dictionary]; + id keyEnumerator = [(NSDictionary *)websiteInfo->attributes keyEnumerator], current; + while ((current = [keyEnumerator nextObject])) + { + [enclosureDictionary setObject:[(NSDictionary *)websiteInfo->attributes objectForKey:current] forKey:current]; + } + [itemDictionaryMutable setObject: enclosureDictionary forKey: itemName]; + continue; + } + + itemValue = [self getelementvalue: itemTree]; + + if ([itemName isEqualTo: @"source"]) + [self flattensourceattributes: itemNode into: itemDictionaryMutable]; + + [itemDictionaryMutable setObject: itemValue forKey: itemName]; + } /*for*/ + + if (normalize) + [self normalizeRSSItem: itemDictionaryMutable]; + + [itemsArrayMutable addObject: itemDictionaryMutable]; + } /*for*/ + + // Sort the news items by published date, descending. + newsItems = [[itemsArrayMutable sortedArrayUsingFunction:compareNewsItems context:NULL] retain]; + } /*createitemsarray*/ + + +- (void) setversionstring: (CFXMLTreeRef) tree { + + CFXMLTreeRef rssTree; + const CFXMLElementInfo *elementInfo; + CFXMLNodeRef node; + + if (flRdf) { + + version = [[NSString alloc] initWithString: @"rdf"]; + + return; + } /*if*/ + + rssTree = [self getnamedtree: tree name: @"rss"]; + + node = CFXMLTreeGetNode (rssTree); + + elementInfo = CFXMLNodeGetInfoPtr (node); + + version = [[NSString alloc] initWithString: [(NSDictionary *) (*elementInfo).attributes objectForKey: @"version"]]; + } /*setversionstring*/ + + +- (void) flattenimagechildren: (CFXMLTreeRef) tree into: (NSMutableDictionary *) dictionary { + + int childCount = CFTreeGetChildCount (tree); + int i = 0; + CFXMLTreeRef childTree; + CFXMLNodeRef childNode; + NSString *childName, *childValue, *keyName; + + if (childCount < 1) + return; + + for (i = 0; i < childCount; i++) { + + childTree = CFTreeGetChildAtIndex (tree, i); + + childNode = CFXMLTreeGetNode (childTree); + + childName = (NSString *) CFXMLNodeGetString (childNode); + + if ([childName hasPrefix: @"rss:"]) + childName = [childName substringFromIndex: 4]; + + childValue = [self getelementvalue: childTree]; + + keyName = [NSString stringWithFormat: @"image%@", childName]; + + [dictionary setObject: childValue forKey: keyName]; + } /*for*/ + } /*flattenimagechildren*/ + + +- (void) flattensourceattributes: (CFXMLNodeRef) node into: (NSMutableDictionary *) dictionary { + + const CFXMLElementInfo *elementInfo; + NSString *sourceHomeUrl, *sourceRssUrl; + + elementInfo = CFXMLNodeGetInfoPtr (node); + + sourceHomeUrl = [(NSDictionary *) (*elementInfo).attributes objectForKey: @"homeUrl"]; + + sourceRssUrl = [(NSDictionary *) (*elementInfo).attributes objectForKey: @"url"]; + + if (sourceHomeUrl != nil) + [dictionary setObject: sourceHomeUrl forKey: @"sourceHomeUrl"]; + + if (sourceRssUrl != nil) + [dictionary setObject: sourceRssUrl forKey: @"sourceRssUrl"]; + } /*flattensourceattributes*/ + + +- (CFXMLTreeRef) getchanneltree: (CFXMLTreeRef) tree { + + CFXMLTreeRef rssTree, channelTree; + + rssTree = [self getnamedtree: tree name: @"rss"]; + + if (rssTree == nil) { /*It might be "rdf:RDF" instead, a 1.0 or greater feed.*/ + + rssTree = [self getnamedtree: tree name: @"rdf:RDF"]; + + if (rssTree != nil) + flRdf = YES; /*This info will be needed later when creating the items array.*/ + } /*if*/ + + if (rssTree == nil) + return (nil); + + channelTree = [self getnamedtree: rssTree name: @"channel"]; + + if (channelTree == nil) + channelTree = [self getnamedtree: rssTree name: @"rss:channel"]; + + return (channelTree); + } /*getchanneltree*/ + + +- (CFXMLTreeRef) getnamedtree: (CFXMLTreeRef) currentTree name: (NSString *) name { + + int childCount, i; + CFXMLNodeRef xmlNode; + CFXMLTreeRef xmlTreeNode; + NSString *itemName; + + childCount = CFTreeGetChildCount (currentTree); + + for (i = childCount - 1; i >= 0; i--) { + + xmlTreeNode = CFTreeGetChildAtIndex (currentTree, i); + + xmlNode = CFXMLTreeGetNode (xmlTreeNode); + + itemName = (NSString *) CFXMLNodeGetString (xmlNode); + + if ([itemName isEqualToString: name]) + return (xmlTreeNode); + } /*for*/ + + return (nil); + } /*getnamedtree*/ + + +- (void) normalizeRSSItem: (NSMutableDictionary *) rssItem { + + /* + Make sure item, link, and description are present and have + reasonable values. Description and link may be "". + Also trim white space, remove HTML when appropriate. + */ + + NSString *description, *link, *title; + BOOL nilDescription = NO; + + /*Description*/ + + description = [rssItem objectForKey: descriptionKey]; + + if (description == nil) { + + description = @""; + + nilDescription = YES; + } /*if*/ + + description = [description trimWhiteSpace]; + + if ([description isEqualTo: @""]) + nilDescription = YES; + + [rssItem setObject: description forKey: descriptionKey]; + + /*Link*/ + + link = [rssItem objectForKey: linkKey]; + + if ([NSString stringIsEmpty: link]) { + + /*Try to get a URL from the description.*/ + + if (!nilDescription) { + + NSArray *stringComponents = [description componentsSeparatedByString: @"href=\""]; + + if ([stringComponents count] > 1) { + + link = [stringComponents objectAtIndex: 1]; + + stringComponents = [link componentsSeparatedByString: @"\""]; + + link = [stringComponents objectAtIndex: 0]; + } /*if*/ + } /*if*/ + } /*if*/ + + if (link == nil) + link = @""; + + link = [link trimWhiteSpace]; + + [rssItem setObject: link forKey: linkKey]; + + /*Title*/ + + title = [rssItem objectForKey: titleKey]; + + if (title != nil) { + + title = [title stripHTML]; + + title = [title trimWhiteSpace]; + } /*if*/ + + if ([NSString stringIsEmpty: title]) { + + /*Grab a title from the description.*/ + + if (!nilDescription) { + + NSArray *stringComponents = [description componentsSeparatedByString: @">"]; + + if ([stringComponents count] > 1) { + + title = [stringComponents objectAtIndex: 1]; + + stringComponents = [title componentsSeparatedByString: @"<"]; + + title = [stringComponents objectAtIndex: 0]; + + title = [title stripHTML]; + + title = [title trimWhiteSpace]; + } /*if*/ + + if ([NSString stringIsEmpty: title]) { /*use first part of description*/ + + NSString *shortTitle = [[[description stripHTML] trimWhiteSpace] ellipsizeAfterNWords: 5]; + + shortTitle = [shortTitle trimWhiteSpace]; + + title = [NSString stringWithFormat: @"%@...", shortTitle]; + } /*else*/ + } /*if*/ + + title = [title stripHTML]; + + title = [title trimWhiteSpace]; + + if ([NSString stringIsEmpty: title]) + title = @"Untitled"; + } /*if*/ + + [rssItem setObject: title forKey: titleKey]; + + /*dangerousmeta case: super-long title with no description*/ + + if ((nilDescription) && ([title length] > 50)) { + + NSString *shortTitle = [[[title stripHTML] trimWhiteSpace] ellipsizeAfterNWords: 7]; + + description = [[title copy] autorelease]; + + [rssItem setObject: description forKey: descriptionKey]; + + title = [NSString stringWithFormat: @"%@...", shortTitle]; + + [rssItem setObject: title forKey: titleKey]; + } /*if*/ + + { /*deal with entities*/ + + const char *tempcstring; + NSAttributedString *s = nil; + NSString *convertedTitle = nil; + NSArray *stringComponents; + + stringComponents = [title componentsSeparatedByString: @"&"]; + + if ([stringComponents count] > 1) { + + stringComponents = [title componentsSeparatedByString: @";"]; + + if ([stringComponents count] > 1) { + + int len; + + tempcstring = [title UTF8String]; + + len = strlen (tempcstring); + + if (len > 0) { + + s = [[NSAttributedString alloc] + initWithHTML: [NSData dataWithBytes: tempcstring length: strlen (tempcstring)] + documentAttributes: (NSDictionary **) NULL]; + + convertedTitle = [s string]; + + [s autorelease]; + + convertedTitle = [convertedTitle stripHTML]; + + convertedTitle = [convertedTitle trimWhiteSpace]; + } /*if*/ + + if ([NSString stringIsEmpty: convertedTitle]) + convertedTitle = @"Untitled"; + + [rssItem setObject: convertedTitle forKey: @"convertedTitle"]; + } /*if*/ + } /*if*/ + } /*deal with entities*/ + } /*normalizeRSSItem*/ + + +- (NSString *) getelementvalue: (CFXMLTreeRef) tree { + + CFXMLNodeRef node; + CFXMLTreeRef itemTree; + int childCount, ix; + NSMutableString *valueMutable; + NSString *value; + NSString *name; + + childCount = CFTreeGetChildCount (tree); + + valueMutable = [[NSMutableString alloc] init]; + + for (ix = 0; ix < childCount; ix++) { + + itemTree = CFTreeGetChildAtIndex (tree, ix); + + node = CFXMLTreeGetNode (itemTree); + + name = (NSString *) CFXMLNodeGetString (node); + + if (name != nil) { + + if (CFXMLNodeGetTypeCode (node) == kCFXMLNodeTypeEntityReference) { + + if ([name isEqualTo: @"lt"]) + name = @"<"; + + else if ([name isEqualTo: @"gt"]) + name = @">"; + + else if ([name isEqualTo: @"quot"]) + name = @"\""; + + else if ([name isEqualTo: @"amp"]) + name = @"&"; + + else if ([name isEqualTo: @"rsquo"]) + name = [NSString stringWithUTF8String:"\u2019"]; + + else if ([name isEqualTo: @"lsquo"]) + name = [NSString stringWithUTF8String:"\u2018"]; + + else if ([name isEqualTo: @"apos"]) + name = @"'"; + else + name = [NSString stringWithFormat: @"&%@;", name]; + } /*if*/ + + [valueMutable appendString: name]; + } /*if*/ + } /*for*/ + + value = [valueMutable copy]; + + [valueMutable autorelease]; + + return ([value autorelease]); + } /*getelementvalue*/ + +@end diff --git a/Release Notes.rtf b/Release Notes.rtf new file mode 100644 index 000000000..d98e36ca9 --- /dev/null +++ b/Release Notes.rtf @@ -0,0 +1,129 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 +{\fonttbl\f0\fswiss\fcharset77 Helvetica-Bold;\f1\fswiss\fcharset77 Helvetica;\f2\fswiss\fcharset77 Helvetica-Oblique; +} +{\colortbl;\red255\green255\blue255;} +{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid1} +{\list\listtemplateid2\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid2} +{\list\listtemplateid3\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid3} +{\list\listtemplateid4\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid4} +{\list\listtemplateid5\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid5} +{\list\listtemplateid6\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid6} +{\list\listtemplateid7\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid7} +{\list\listtemplateid8\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid8} +{\list\listtemplateid9\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid9}} +{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}{\listoverride\listid5\listoverridecount0\ls5}{\listoverride\listid6\listoverridecount0\ls6}{\listoverride\listid7\listoverridecount0\ls7}{\listoverride\listid8\listoverridecount0\ls8}{\listoverride\listid9\listoverridecount0\ls9}} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural + +\f0\b\fs24 \cf0 Sparkle Updater +\f1\b0 \ +Release Notes\ +\ +Version 1.1\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls1\ilvl0\cf0 {\listtext \'a5 }Optimized framework size: now only 1.4mb with all localizations and 384kb with only English (an English-only version is in the Extras folder).\ +{\listtext \'a5 }Added a new SUStatusChecker class for programmatically determining if a new version is available (see the docs); thanks, Evan Schoenberg!\ +{\listtext \'a5 }Added support for apps using SIGCHLD; thanks, Augie Fackler!\ +{\listtext \'a5 }Added a zh_CN update from JT Lee\ +{\listtext \'a5 }Added a Polish update from Piotr Chylinski\ +{\listtext \'a5 }Fixed DMG support for images with /Applications symlinks.\ +{\listtext \'a5 }Fixed a really stupid interval-checking bug that could cause repeated hits to the appcast.\ +{\listtext \'a5 }Fixed a bug where the check interval would be inconsistent if a value of 0 was stored in the user defaults.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural +\cf0 \ +Version 1.0\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls2\ilvl0\cf0 {\listtext \'a5 }Additions:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls3\ilvl1\cf0 {\listtext \uc0\u8259 }Added real version comparison courtesy Kevin Ballard: Sparkle now knows that 0.89 < 1.0a3 < 1.0.\ +{\listtext \uc0\u8259 }Added many localizations courtesy David Kocher's localization team.\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Added a much better installation mechanism courtesy Allan Odgaard.\ +{\listtext \uc0\u8259 }Added a user agent string to the RSS fetch request.\ +{\listtext \uc0\u8259 }Added support for CFBundleShortVersionString in addition to CFBundleVersion, and support for a sparkle:shortVersionString attribute on the enclosure.\ +{\listtext \uc0\u8259 }Added support for CFBundleDisplayName if available.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls4\ilvl0\cf0 {\listtext \'a5 }Changes:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Automatic updating is now allowed by default, but only if DSA signing is on.\ +{\listtext \uc0\u8259 }Pressing Escape or closing the update alert now reminds the user later.\ +{\listtext \uc0\u8259 }Now when there's a stored check interval, Sparkle doesn't check immediately on startup the first time the app is launched because the user hasn't consented to it yet.\ +{\listtext \uc0\u8259 }The update alert now remembers its size and floats.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls4\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Fixed installation of DMGs with multiple files enclosed.\ +{\listtext \uc0\u8259 }Fixed a nasty memory leak.\ +{\listtext \uc0\u8259 }Fixed a bug wherein having no value for allowing automatic updates would display a checkbox for the updates but would not honor it.\ +{\listtext \uc0\u8259 }Fixed a bug in zip extraction that occurred in Panther.\ +{\listtext \uc0\u8259 }Fixed release notes caching.\ +{\listtext \uc0\u8259 }Fixed a bug wherein Sparkle refused to authenticate the installation if the user had cancelled authentication previously in that session.\ +{\listtext \uc0\u8259 }Fixed a weird bug that would cause a second help menu to appear on first launch.\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls5\ilvl1\cf0 {\listtext \uc0\u8259 }Fixed a bug that could occur when changing the scheduled check interval.\ +{\listtext \uc0\u8259 }Fixed a bug wherein the host app could crash if the user clicked Remind Me Later before the release notes finished loading.\ +{\listtext \uc0\u8259 }Fixed a bug wherein the behavior was undefined if the user manually initiated a check when an automatic one was already taking place.\ +{\listtext \uc0\u8259 }Fixed wrapping on the description field in the update alert.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural +\cf0 \ +Version 1.0 (beta 3):\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls6\ilvl0\cf0 {\listtext \'a5 }Fixed a +\f2\i nasty +\f1\i0 crasher that occurred often when the user was not connected to the internet.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural +\cf0 \ +Version 1.0 (beta 2):\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls7\ilvl0\cf0 {\listtext \'a5 }Major Improvements:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }Fully automatic updating! (see the Documentation: this is beta and off by default)\ +{\listtext \uc0\u8259 }Added support for DSA signatures (see the Documentation).\ +{\listtext \uc0\u8259 }Added support for MD5 sum verification.\ +{\listtext \uc0\u8259 }Added Security.framework-based authentication for installing to privileged directories.\ +{\listtext \uc0\u8259 }Huge refactoring of the codebase: there's now a Sparkle Xcode project, Sparkle is now a framework, and everything is modular / abstracted. And no more code-generated interface.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls7\ilvl0\cf0 {\listtext \'a5 }Minor Improvements:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }A SUUpdaterWillRestartNotification is sent out before restarting now.\ +{\listtext \uc0\u8259 }Added key equivalents to alert panel buttons.\ +{\listtext \uc0\u8259 }Error handling is much prettier now: technical messages are not presented to the user anymore.\ +{\listtext \uc0\u8259 }There's now a test app for developers to see what Sparkle's like before using it.\ +{\listtext \uc0\u8259 }Wrote new, pretty, extremely thorough documentation.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls7\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }Relaunch behavior is much improved and shouldn't fail in huge apps anymore.\ +{\listtext \uc0\u8259 }Fixed a bug wherein a failing tar command could crash the host app.\ +{\listtext \uc0\u8259 }Sparkle now looks at InfoPlist.strings in addition to Info.plist.\ +{\listtext \uc0\u8259 }Fixed some stupid typos.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural +\cf0 \ +Version 1.0 (beta 1):\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls8\ilvl0\cf0 {\listtext \'a5 }Major New Features:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Sparkle now supports scheduled periodic updates\'d1read the Readme for information on how to use it.\ +{\listtext \uc0\u8259 }Sparkle now supports WebKit-based release notes (for CSS and full HTML), which it displays in the main update alert, not a separate panel. The Readme has much more information. Sparkle will, of course, fall back on NSTextView if the host app does not include WebKit.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls8\ilvl0\cf0 {\listtext \'a5 }Minor New Features:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Added support for .zip update archives.\ +{\listtext \uc0\u8259 }Added support for .dmg update archives.\ +{\listtext \uc0\u8259 }Implemented Remind Me Later to replace simple update cancellation.\ +{\listtext \uc0\u8259 }Implemented Skip This Version functionality.\ +{\listtext \uc0\u8259 }Added support for multiple feeds via the user defaults SUFeedURL key taking precedent over the one in Info.plist.\ +{\listtext \uc0\u8259 }Added support for Sparkle's custom XML namespace, which is optional but may prove useful. See the Readme for more information.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls8\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\ +\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural +\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Sparkle will no longer enter an inconsistent state if the user tries to update again while one is already in progress.\ +{\listtext \uc0\u8259 }Sparkle now uses CFBundleName to determine the application's name instead of the app's filename.\ +{\listtext \uc0\u8259 }Sparkle no longer crashes if the user cancels during extraction.\ +{\listtext \uc0\u8259 }Lots of code refactoring.\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural +\cf0 \ +Version 0.1:\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural +\ls9\ilvl0\cf0 {\listtext \'a5 }Initial Release\ +} \ No newline at end of file diff --git a/SUAppcast.h b/SUAppcast.h new file mode 100644 index 000000000..209fe2061 --- /dev/null +++ b/SUAppcast.h @@ -0,0 +1,27 @@ +// +// SUAppcast.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +@class RSS, SUAppcastItem; +@interface SUAppcast : NSObject { + NSArray *items; + id delegate; +} + +- (void)fetchAppcastFromURL:(NSURL *)url; +- (void)setDelegate:delegate; + +- (SUAppcastItem *)newestItem; +- (NSArray *)items; + +@end + +@interface NSObject (SUAppcastDelegate) +- appcastDidFinishLoading:(SUAppcast *)appcast; +@end \ No newline at end of file diff --git a/SUAppcast.m b/SUAppcast.m new file mode 100644 index 000000000..aea968deb --- /dev/null +++ b/SUAppcast.m @@ -0,0 +1,80 @@ +// +// SUAppcast.m +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUAppcast.h" +#import "SUAppcastItem.h" +#import "SUUtilities.h" +#import "RSS.h" + +@implementation SUAppcast + +- (void)fetchAppcastFromURL:(NSURL *)url +{ + [NSThread detachNewThreadSelector:@selector(_fetchAppcastFromURL:) toTarget:self withObject:url]; // let's not block the main thread +} + +- (void)setDelegate:del +{ + delegate = del; +} + +- (void)dealloc +{ + [items release]; + [super dealloc]; +} + +- (SUAppcastItem *)newestItem +{ + return [items objectAtIndex:0]; // the RSS class takes care of sorting by published date, descending. +} + +- (NSArray *)items +{ + return items; +} + +- (void)_fetchAppcastFromURL:(NSURL *)url +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + RSS *feed; + @try + { + NSString *userAgent = [NSString stringWithFormat: @"%@/%@ (Mac OS X) Sparkle/1.0", SUHostAppName(), SUHostAppVersion()]; + + feed = [[RSS alloc] initWithURL:url normalize:YES userAgent:userAgent]; + if (!feed) + [NSException raise:@"SUFeedException" format:@"Couldn't fetch feed from server."]; + + // Set up all the appcast items + NSMutableArray *tempItems = [NSMutableArray array]; + id enumerator = [[feed newsItems] objectEnumerator], current; + while ((current = [enumerator nextObject])) + { + [tempItems addObject:[[[SUAppcastItem alloc] initWithDictionary:current] autorelease]]; + } + items = [[NSArray arrayWithArray:tempItems] retain]; + [feed release]; + + if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:)]) + [delegate performSelectorOnMainThread:@selector(appcastDidFinishLoading:) withObject:self waitUntilDone:NO]; + + } + @catch (NSException *e) + { + if ([delegate respondsToSelector:@selector(appcastDidFailToLoad:)]) + [delegate performSelectorOnMainThread:@selector(appcastDidFailToLoad:) withObject:self waitUntilDone:NO]; + } + @finally + { + [pool release]; + } +} + +@end diff --git a/SUAppcastItem.h b/SUAppcastItem.h new file mode 100644 index 000000000..908707341 --- /dev/null +++ b/SUAppcastItem.h @@ -0,0 +1,62 @@ +// +// SUAppcastItem.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + + +@interface SUAppcastItem : NSObject { + NSString *title; + NSDate *date; + NSString *description; + + NSURL *releaseNotesURL; + + NSString *DSASignature; + NSString *MD5Sum; + + NSString *minimumSystemVersion; + + NSURL *fileURL; + NSString *fileVersion; + NSString *versionString; +} + +// Initializes with data from a dictionary provided by the RSS class. +- initWithDictionary:(NSDictionary *)dict; + +- (NSString *)title; +- (void)setTitle:(NSString *)aTitle; + +- (NSDate *)date; +- (void)setDate:(NSDate *)aDate; + +- (NSString *)description; +- (void)setDescription:(NSString *)aDescription; + +- (NSURL *)releaseNotesURL; +- (void)setReleaseNotesURL:(NSURL *)aReleaseNotesURL; + +- (NSString *)DSASignature; +- (void)setDSASignature:(NSString *)aDSASignature; + +- (NSString *)MD5Sum; +- (void)setMD5Sum:(NSString *)aMd5Sum; + +- (NSURL *)fileURL; +- (void)setFileURL:(NSURL *)aFileURL; + +- (NSString *)fileVersion; +- (void)setFileVersion:(NSString *)aFileVersion; + +- (NSString *)versionString; +- (void)setVersionString:(NSString *)versionString; + +- (NSString *)minimumSystemVersion; +- (void)setMinimumSystemVersion:(NSString *)systemVersionString; + +@end diff --git a/SUAppcastItem.m b/SUAppcastItem.m new file mode 100644 index 000000000..4e2090acf --- /dev/null +++ b/SUAppcastItem.m @@ -0,0 +1,184 @@ +// +// SUAppcastItem.m +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUAppcastItem.h" + + +@implementation SUAppcastItem + +- initWithDictionary:(NSDictionary *)dict +{ + [super init]; + [self setTitle:[dict objectForKey:@"title"]]; + [self setDate:[dict objectForKey:@"pubDate"]]; + [self setDescription:[dict objectForKey:@"description"]]; + + id enclosure = [dict objectForKey:@"enclosure"]; + [self setDSASignature:[enclosure objectForKey:@"sparkle:dsaSignature"]]; + [self setMD5Sum:[enclosure objectForKey:@"sparkle:md5Sum"]]; + + [self setFileURL:[NSURL URLWithString:[[enclosure objectForKey:@"url"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; + + // Find the appropriate release notes URL. + if ([dict objectForKey:@"sparkle:releaseNotesLink"]) + { + [self setReleaseNotesURL:[NSURL URLWithString:[dict objectForKey:@"sparkle:releaseNotesLink"]]]; + } + else if ([[self description] hasPrefix:@"http://"]) // if the description starts with http://, use that. + { + [self setReleaseNotesURL:[NSURL URLWithString:[self description]]]; + } + else + { + [self setReleaseNotesURL:nil]; + } + + NSString *minVersion = [dict objectForKey:@"sparkle:minimumSystemVersion"]; + if(minVersion) + [self setMinimumSystemVersion:minVersion]; + else + [self setMinimumSystemVersion:@"10.3.0"];//sparkle doesn't run on 10.2-, so we don't have to worry about it + + // Try to find a version string. + // Finding the new version number from the RSS feed is a little bit hacky. There are two ways: + // 1. A "sparkle:version" attribute on the enclosure tag, an extension from the RSS spec. + // 2. If there isn't a version attribute, Sparkle will parse the path in the enclosure, expecting + // that it will look like this: http://something.com/YourApp_0.5.zip. It'll read whatever's between the last + // underscore and the last period as the version number. So name your packages like this: APPNAME_VERSION.extension. + // The big caveat with this is that you can't have underscores in your version strings, as that'll confuse Sparkle. + // Feel free to change the separator string to a hyphen or something more suited to your needs if you like. + NSString *newVersion = [enclosure objectForKey:@"sparkle:version"]; + if (!newVersion) // no sparkle:version attribute + { + // Separate the url by underscores and take the last component, as that'll be closest to the end, + // then we remove the extension. Hopefully, this will be the version. + NSArray *fileComponents = [[enclosure objectForKey:@"url"] componentsSeparatedByString:@"_"]; + if ([fileComponents count] > 1) + newVersion = [[fileComponents lastObject] stringByDeletingPathExtension]; + } + [self setFileVersion:newVersion]; + + NSString *shortVersionString = [enclosure objectForKey:@"sparkle:shortVersionString"]; + if (shortVersionString) + { + if (![[self fileVersion] isEqualToString:shortVersionString]) + shortVersionString = [shortVersionString stringByAppendingFormat:@"/%@", [self fileVersion]]; + [self setVersionString:shortVersionString]; + } + else + [self setVersionString:[self fileVersion]]; + + return self; +} + +// Attack of accessors! + +- (NSString *)title { return [[title retain] autorelease]; } + +- (void)setTitle:(NSString *)aTitle +{ + [title release]; + title = [aTitle copy]; +} + + +- (NSDate *)date { return [[date retain] autorelease]; } + +- (void)setDate:(NSDate *)aDate +{ + [date release]; + date = [aDate copy]; +} + + +- (NSString *)description { return [[description retain] autorelease]; } + +- (void)setDescription:(NSString *)aDescription +{ + [description release]; + description = [aDescription copy]; +} + + +- (NSURL *)releaseNotesURL { return [[releaseNotesURL retain] autorelease]; } + +- (void)setReleaseNotesURL:(NSURL *)aReleaseNotesURL +{ + [releaseNotesURL release]; + releaseNotesURL = [aReleaseNotesURL copy]; +} + + +- (NSString *)DSASignature { return [[DSASignature retain] autorelease]; } + +- (void)setDSASignature:(NSString *)aDSASignature +{ + [DSASignature release]; + DSASignature = [aDSASignature copy]; +} + + +- (NSString *)MD5Sum { return [[MD5Sum retain] autorelease]; } + +- (void)setMD5Sum:(NSString *)aMD5Sum +{ + [MD5Sum release]; + MD5Sum = [aMD5Sum copy]; +} + + +- (NSURL *)fileURL { return [[fileURL retain] autorelease]; } + +- (void)setFileURL:(NSURL *)aFileURL +{ + [fileURL release]; + fileURL = [aFileURL copy]; +} + + +- (NSString *)fileVersion { return [[fileVersion retain] autorelease]; } + +- (void)setFileVersion:(NSString *)aFileVersion +{ + [fileVersion release]; + fileVersion = [aFileVersion copy]; +} + + +- (NSString *)versionString { return [[versionString retain] autorelease]; } + +- (void)setVersionString:(NSString *)aVersionString +{ + [versionString release]; + versionString = [aVersionString copy]; +} + + +- (NSString *)minimumSystemVersion { return [[minimumSystemVersion retain] autorelease]; } +- (void)setMinimumSystemVersion:(NSString *)systemVersionString +{ + [minimumSystemVersion release]; + minimumSystemVersion = [systemVersionString copy]; +} + + +- (void)dealloc +{ + [self setTitle:nil]; + [self setDate:nil]; + [self setDescription:nil]; + [self setReleaseNotesURL:nil]; + [self setDSASignature:nil]; + [self setMD5Sum:nil]; + [self setFileURL:nil]; + [self setFileVersion:nil]; + [self setVersionString:nil]; + [super dealloc]; +} + +@end diff --git a/SUAutomaticUpdateAlert.h b/SUAutomaticUpdateAlert.h new file mode 100644 index 000000000..fc0ac9fd0 --- /dev/null +++ b/SUAutomaticUpdateAlert.h @@ -0,0 +1,21 @@ +// +// SUAutomaticUpdateAlert.h +// Sparkle +// +// Created by Andy Matuschak on 3/18/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +@class SUAppcastItem; +@interface SUAutomaticUpdateAlert : NSWindowController { + SUAppcastItem *updateItem; +} + +- initWithAppcastItem:(SUAppcastItem *)item; + +- (IBAction)relaunchNow:sender; +- (IBAction)relaunchLater:sender; + +@end diff --git a/SUAutomaticUpdateAlert.m b/SUAutomaticUpdateAlert.m new file mode 100644 index 000000000..1e89f4657 --- /dev/null +++ b/SUAutomaticUpdateAlert.m @@ -0,0 +1,61 @@ +// +// SUAutomaticUpdateAlert.m +// Sparkle +// +// Created by Andy Matuschak on 3/18/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUAutomaticUpdateAlert.h" +#import "SUUtilities.h" +#import "SUAppcastItem.h" + +@implementation SUAutomaticUpdateAlert + +- initWithAppcastItem:(SUAppcastItem *)item +{ + NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"SUAutomaticUpdateAlert" ofType:@"nib"]; + if (!path) // slight hack to resolve issues with running with in configurations + { + NSBundle *current = [NSBundle bundleForClass:[self class]]; + NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingFormat:@"/Sparkle.framework", [current bundleIdentifier]]; + NSBundle *framework = [NSBundle bundleWithPath:frameworkPath]; + path = [framework pathForResource:@"SUAutomaticUpdateAlert" ofType:@"nib"]; + } + + [super initWithWindowNibPath:path owner:self]; + + updateItem = [item retain]; + [self setShouldCascadeWindows:NO]; + + return self; +} + +- (IBAction)relaunchNow:sender +{ + [self close]; + [NSApp stopModalWithCode:NSAlertDefaultReturn]; +} + +- (IBAction)relaunchLater:sender +{ + [self close]; + [NSApp stopModalWithCode:NSAlertAlternateReturn]; +} + +- (NSImage *)applicationIcon +{ + return [NSImage imageNamed:@"NSApplicationIcon"]; +} + +- (NSString *)titleText +{ + return [NSString stringWithFormat:SULocalizedString(@"A new version of %@ has been installed!", nil), SUHostAppDisplayName()]; +} + +- (NSString *)descriptionText +{ + return [NSString stringWithFormat:SULocalizedString(@"%@ %@ has been installed and will be ready to use next time %@ starts! Would you like to relaunch now?", nil), SUHostAppDisplayName(), [updateItem versionString], SUHostAppDisplayName()]; +} + +@end diff --git a/SUConstants.h b/SUConstants.h new file mode 100644 index 000000000..b0ac4e36d --- /dev/null +++ b/SUConstants.h @@ -0,0 +1,20 @@ +// +// SUConstants.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +extern NSString *SUUpdaterWillRestartNotification; + +extern NSString *SUCheckAtStartupKey; +extern NSString *SUFeedURLKey; +extern NSString *SUShowReleaseNotesKey; +extern NSString *SUSkippedVersionKey; +extern NSString *SUScheduledCheckIntervalKey; +extern NSString *SULastCheckTimeKey; +extern NSString *SUExpectsDSASignatureKey; +extern NSString *SUPublicDSAKeyKey; +extern NSString *SUAutomaticallyUpdateKey; +extern NSString *SUAllowsAutomaticUpdatesKey; diff --git a/SUConstants.m b/SUConstants.m new file mode 100644 index 000000000..3399ab47e --- /dev/null +++ b/SUConstants.m @@ -0,0 +1,20 @@ +// +// SUConstants.m +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +NSString *SUUpdaterWillRestartNotification = @"SUUpdaterWillRestartNotificationName"; + +NSString *SUCheckAtStartupKey = @"SUCheckAtStartup"; +NSString *SUFeedURLKey = @"SUFeedURL"; +NSString *SUShowReleaseNotesKey = @"SUShowReleaseNotes"; +NSString *SUSkippedVersionKey = @"SUSkippedVersion"; +NSString *SUScheduledCheckIntervalKey = @"SUScheduledCheckInterval"; +NSString *SULastCheckTimeKey = @"SULastCheckTime"; +NSString *SUExpectsDSASignatureKey = @"SUExpectsDSASignature"; +NSString *SUPublicDSAKeyKey = @"SUPublicDSAKey"; +NSString *SUAutomaticallyUpdateKey = @"SUAutomaticallyUpdate"; +NSString *SUAllowsAutomaticUpdatesKey = @"SUAllowsAutomaticUpdates"; \ No newline at end of file diff --git a/SUStatusChecker.h b/SUStatusChecker.h new file mode 100644 index 000000000..e83d15206 --- /dev/null +++ b/SUStatusChecker.h @@ -0,0 +1,26 @@ +// +// SUStatusChecker.h +// Sparkle +// +// Created by Evan Schoenberg on 7/6/06. +// + +#import +#import + +@class SUStatusChecker; + +@protocol SUStatusCheckerDelegate +//versionString will be nil and isNewVersion will be NO if version checking fails. +- (void)statusChecker:(SUStatusChecker *)statusChecker foundVersion:(NSString *)versionString isNewVersion:(BOOL)isNewVersion; +@end + +@interface SUStatusChecker : SUUpdater { + id scDelegate; +} + +// Create a status checker which will notifiy delegate once the appcast version is determined. +// Notification occurs via the method defined in the SUStatusCheckerDelegate informal protocol. ++ (SUStatusChecker *)statusCheckerForDelegate:(id)delegate; + +@end diff --git a/SUStatusChecker.m b/SUStatusChecker.m new file mode 100644 index 000000000..0bc341c55 --- /dev/null +++ b/SUStatusChecker.m @@ -0,0 +1,78 @@ +// +// SUStatusChecker.m +// Sparkle +// +// Created by Evan Schoenberg on 7/6/06. +// + +#import "SUStatusChecker.h" +#import "SUAppcast.h" +#import "SUAppcastItem.h" + +@interface SUStatusChecker (Private) +- (id)initForDelegate:(id)inDelegate; +- (void)checkForUpdatesAndNotify:(BOOL)verbosity; +- (BOOL)newVersionAvailable; +@end; + +@implementation SUStatusChecker + ++ (SUStatusChecker *)statusCheckerForDelegate:(id)inDelegate; +{ + SUStatusChecker *statusChecker = [[self alloc] initForDelegate:inDelegate]; + + return [statusChecker autorelease]; +} + +- (id)initForDelegate:(id)inDelegate +{ + [super init]; + + scDelegate = [inDelegate retain]; + + [self checkForUpdatesAndNotify:NO]; + + return self; +} + +- (void)dealloc +{ + [scDelegate release]; scDelegate = nil; + + [super dealloc]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)note +{ + //Take no action when the application finishes launching +} + +- (void)appcastDidFinishLoading:(SUAppcast *)ac +{ + @try + { + if (!ac) { [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."]; } + + updateItem = [[ac newestItem] retain]; + [ac autorelease]; + + if (![updateItem fileVersion]) + { + [NSException raise:@"SUAppcastException" format:@"Can't extract a version string from the appcast feed. The filenames should look like YourApp_1.5.tgz, where 1.5 is the version number."]; + } + + [scDelegate statusChecker:self + foundVersion:[updateItem fileVersion] + isNewVersion:[self newVersionAvailable]]; + } + @catch (NSException *e) + { + NSLog([e reason]); + + [scDelegate statusChecker:self foundVersion:nil isNewVersion:NO]; + } + + updateInProgress = NO; +} + +@end diff --git a/SUStatusController.h b/SUStatusController.h new file mode 100644 index 000000000..19a3f89ec --- /dev/null +++ b/SUStatusController.h @@ -0,0 +1,33 @@ +// +// SUStatusController.h +// Sparkle +// +// Created by Andy Matuschak on 3/14/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + + +@interface SUStatusController : NSWindowController { + double progressValue, maxProgressValue; + NSString *title, *statusText, *buttonTitle; + IBOutlet NSButton *actionButton; +} + +// Pass 0 for the max progress value to get an indeterminate progress bar. +// Pass nil for the status text to not show it. +- (void)beginActionWithTitle:(NSString *)title maxProgressValue:(double)maxProgressValue statusText:(NSString *)statusText; + +// If isDefault is YES, the button's key equivalent will be \r. +- (void)setButtonTitle:(NSString *)buttonTitle target:target action:(SEL)action isDefault:(BOOL)isDefault; +- (void)setButtonEnabled:(BOOL)enabled; + +- (double)progressValue; +- (void)setProgressValue:(double)value; +- (double)maxProgressValue; +- (void)setMaxProgressValue:(double)value; + +- (void)setStatusText:(NSString *)statusText; + +@end diff --git a/SUStatusController.m b/SUStatusController.m new file mode 100644 index 000000000..909a8696c --- /dev/null +++ b/SUStatusController.m @@ -0,0 +1,119 @@ +// +// SUStatusController.m +// Sparkle +// +// Created by Andy Matuschak on 3/14/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUStatusController.h" +#import "SUUtilities.h" + +@implementation SUStatusController + +- init +{ + NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"SUStatus" ofType:@"nib"]; + if (!path) // slight hack to resolve issues with running in debug configurations + { + NSBundle *current = [NSBundle bundleForClass:[self class]]; + NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingFormat:@"/Sparkle.framework", [current bundleIdentifier]]; + NSBundle *framework = [NSBundle bundleWithPath:frameworkPath]; + path = [framework pathForResource:@"SUStatus" ofType:@"nib"]; + } + [super initWithWindowNibPath:path owner:self]; + [self setShouldCascadeWindows:NO]; + return self; +} + +- (void)dealloc +{ + [title release]; + [statusText release]; + [buttonTitle release]; + [super dealloc]; +} + +- (void)awakeFromNib +{ + [[self window] center]; + [[self window] setFrameAutosaveName:@"SUStatusFrame"]; +} + +- (NSString *)windowTitle +{ + return [NSString stringWithFormat:SULocalizedString(@"Updating %@", nil), SUHostAppDisplayName()]; +} + +- (NSImage *)applicationIcon +{ + return [NSImage imageNamed:@"NSApplicationIcon"]; +} + +- (void)beginActionWithTitle:(NSString *)aTitle maxProgressValue:(double)aMaxProgressValue statusText:(NSString *)aStatusText +{ + [self willChangeValueForKey:@"title"]; + title = [aTitle copy]; + [self didChangeValueForKey:@"title"]; + + [self setMaxProgressValue:aMaxProgressValue]; + [self setStatusText:aStatusText]; +} + +- (void)setButtonTitle:(NSString *)aButtonTitle target:target action:(SEL)action isDefault:(BOOL)isDefault +{ + [self willChangeValueForKey:@"buttonTitle"]; + buttonTitle = [aButtonTitle copy]; + [self didChangeValueForKey:@"buttonTitle"]; + + [actionButton sizeToFit]; + // Except we're going to add 15 px for padding. + [actionButton setFrameSize:NSMakeSize([actionButton frame].size.width + 15, [actionButton frame].size.height)]; + // Now we have to move it over so that it's always 15px from the side of the window. + [actionButton setFrameOrigin:NSMakePoint([[self window] frame].size.width - 15 - [actionButton frame].size.width, [actionButton frame].origin.y)]; + // Redisplay superview to clean up artifacts + [[actionButton superview] display]; + + [actionButton setTarget:target]; + [actionButton setAction:action]; + [actionButton setKeyEquivalent:isDefault ? @"\r" : @""]; +} + +- (void)setButtonEnabled:(BOOL)enabled +{ + [actionButton setEnabled:enabled]; +} + +- (double)progressValue +{ + return progressValue; +} + +- (void)setProgressValue:(double)value +{ + [self willChangeValueForKey:@"progressValue"]; + progressValue = value; + [self didChangeValueForKey:@"progressValue"]; +} + +- (double)maxProgressValue +{ + return maxProgressValue; +} + +- (void)setMaxProgressValue:(double)value +{ + [self willChangeValueForKey:@"maxProgressValue"]; + maxProgressValue = value; + [self didChangeValueForKey:@"maxProgressValue"]; + [self setProgressValue:0]; +} + +- (void)setStatusText:(NSString *)aStatusText +{ + [self willChangeValueForKey:@"statusText"]; + statusText = [aStatusText copy]; + [self didChangeValueForKey:@"statusText"]; +} + +@end diff --git a/SUUnarchiver.h b/SUUnarchiver.h new file mode 100644 index 000000000..bb8905e65 --- /dev/null +++ b/SUUnarchiver.h @@ -0,0 +1,25 @@ +// +// SUUnarchiver.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + + +@interface SUUnarchiver : NSObject { + id delegate; +} + +- (void)unarchivePath:(NSString *)path; +- (void)setDelegate:delegate; + +@end + +@interface NSObject (SUUnarchiverDelegate) +- (void)unarchiver:(SUUnarchiver *)unarchiver extractedLength:(long)length; +- (void)unarchiverDidFinish:(SUUnarchiver *)unarchiver; +- (void)unarchiverDidFail:(SUUnarchiver *)unarchiver; +@end diff --git a/SUUnarchiver.m b/SUUnarchiver.m new file mode 100644 index 000000000..e90505f9e --- /dev/null +++ b/SUUnarchiver.m @@ -0,0 +1,144 @@ +// +// SUUnarchiver.m +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUUnarchiver.h" + + +@implementation SUUnarchiver + +// This method abstracts the types that use a command line tool piping data from stdin. +- (BOOL)_extractArchivePath:archivePath pipingDataToCommand:(NSString *)command +{ + // Get the file size. + NSNumber *fs = [[[NSFileManager defaultManager] fileAttributesAtPath:archivePath traverseLink:NO] objectForKey:NSFileSize]; + if (fs == nil) { return NO; } + + // Thank you, Allan Odgaard! + // (who wrote the following extraction alg.) + + long current = 0; + FILE *fp, *cmdFP; + sig_t oldSigPipeHandler = signal(SIGPIPE, SIG_IGN); + if ((fp = fopen([archivePath UTF8String], "r"))) + { + setenv("DESTINATION", [[archivePath stringByDeletingLastPathComponent] UTF8String], 1); + if ((cmdFP = popen([command cString], "w"))) + { + char buf[32*1024]; + long len; + while((len = fread(buf, 1, 32 * 1024, fp))) + { + current += len; + + NSEvent *event; + while((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])) + [NSApp sendEvent:event]; + + fwrite(buf, 1, len, cmdFP); + + if ([delegate respondsToSelector:@selector(unarchiver:extractedLength:)]) + [delegate unarchiver:self extractedLength:len]; + } + pclose(cmdFP); + } + fclose(fp); + } + signal(SIGPIPE, oldSigPipeHandler); + return YES; +} + +- (BOOL)_extractTAR:(NSString *)archivePath +{ + return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -xC \"$DESTINATION\""]; +} + +- (BOOL)_extractTGZ:(NSString *)archivePath +{ + return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -zxC \"$DESTINATION\""]; +} + +- (BOOL)_extractTBZ:(NSString *)archivePath +{ + return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -jxC \"$DESTINATION\""]; +} + +- (BOOL)_extractZIP:(NSString *)archivePath +{ + return [self _extractArchivePath:archivePath pipingDataToCommand:@"ditto -x -k - \"$DESTINATION\""]; +} + +- (BOOL)_extractDMG:(NSString *)archivePath +{ + sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); + // First, we internet-enable the volume. + NSTask *hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"internet-enable", @"-quiet", archivePath, nil]]; + [hdiTask waitUntilExit]; + if ([hdiTask terminationStatus] != 0) { return NO; } + + // Now, open the volume; it'll extract into its own directory. + hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"attach", @"-idme", @"-noidmereveal", @"-noidmetrash", @"-noverify", @"-nobrowse", @"-noautoopen", @"-quiet", archivePath, nil]]; + [hdiTask waitUntilExit]; + if ([hdiTask terminationStatus] != 0) { return NO; } + + signal(SIGCHLD, oldSigChildHandler); + + return YES; +} + +- (void)_unarchivePath:(NSString *)path +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + // This dictionary associates names of methods responsible for extraction with file extensions. + // The methods take the path of the archive to extract. They return a BOOL indicating whether + // we should continue with the update; returns NO if an error occurred. + NSDictionary *commandDictionary = [NSDictionary dictionaryWithObjectsAndKeys: + @"_extractTBZ:", @"tbz", + @"_extractTGZ:", @"tgz", + @"_extractTAR:", @"tar", + @"_extractZIP:", @"zip", + @"_extractDMG:", @"dmg", nil]; + SEL command = NSSelectorFromString([commandDictionary objectForKey:[path pathExtension]]); + + BOOL result; + if (command) + { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:command]]; + [invocation setSelector:command]; + [invocation setArgument:&path atIndex:2]; // 0 and 1 are private! + [invocation invokeWithTarget:self]; + [invocation getReturnValue:&result]; + } + else + result = NO; + + if (result) + { + if ([delegate respondsToSelector:@selector(unarchiverDidFinish:)]) + [delegate performSelector:@selector(unarchiverDidFinish:) withObject:self]; + } + else + { + if ([delegate respondsToSelector:@selector(unarchiverDidFail:)]) + [delegate performSelector:@selector(unarchiverDidFail:) withObject:self]; + } + + [pool release]; +} + +- (void)unarchivePath:(NSString *)path +{ + [NSThread detachNewThreadSelector:@selector(_unarchivePath:) toTarget:self withObject:path]; +} + +- (void)setDelegate:del +{ + delegate = del; +} + +@end diff --git a/SUUpdateAlert.h b/SUUpdateAlert.h new file mode 100644 index 000000000..69c281749 --- /dev/null +++ b/SUUpdateAlert.h @@ -0,0 +1,40 @@ +// +// SUUpdateAlert.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +typedef enum +{ + SUInstallUpdateChoice, + SURemindMeLaterChoice, + SUSkipThisVersionChoice +} SUUpdateAlertChoice; + +@class WebView, SUAppcastItem; +@interface SUUpdateAlert : NSWindowController { + SUAppcastItem *updateItem; + id delegate; + + IBOutlet WebView *releaseNotesView; + IBOutlet NSTextField *description; + NSProgressIndicator *releaseNotesSpinner; + BOOL webViewFinishedLoading; +} + +- initWithAppcastItem:(SUAppcastItem *)item; +- (void)setDelegate:delegate; + +- (IBAction)installUpdate:sender; +- (IBAction)skipThisVersion:sender; +- (IBAction)remindMeLater:sender; + +@end + +@interface NSObject (SUUpdateAlertDelegate) +- (void)updateAlert:(SUUpdateAlert *)updateAlert finishedWithChoice:(SUUpdateAlertChoice)updateChoice; +@end diff --git a/SUUpdateAlert.m b/SUUpdateAlert.m new file mode 100644 index 000000000..ba1b1c11e --- /dev/null +++ b/SUUpdateAlert.m @@ -0,0 +1,185 @@ +// +// SUUpdateAlert.m +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUUpdateAlert.h" +#import "SUAppcastItem.h" +#import "SUUtilities.h" +#import + +@implementation SUUpdateAlert + +- initWithAppcastItem:(SUAppcastItem *)item +{ + NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"SUUpdateAlert" ofType:@"nib"]; + if (!path) // slight hack to resolve issues with running with in configurations + { + NSBundle *current = [NSBundle bundleForClass:[self class]]; + NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingFormat:@"/Sparkle.framework", [current bundleIdentifier]]; + NSBundle *framework = [NSBundle bundleWithPath:frameworkPath]; + path = [framework pathForResource:@"SUUpdateAlert" ofType:@"nib"]; + } + + [super initWithWindowNibPath:path owner:self]; + + updateItem = [item retain]; + [self setShouldCascadeWindows:NO]; + + return self; +} + +- (void)dealloc +{ + [updateItem release]; + [super dealloc]; +} + +- (void)endWithSelection:(SUUpdateAlertChoice)choice +{ + [releaseNotesView stopLoading:self]; + [releaseNotesView setFrameLoadDelegate:nil]; + [releaseNotesView setPolicyDelegate:nil]; + [self close]; + if ([delegate respondsToSelector:@selector(updateAlert:finishedWithChoice:)]) + [delegate updateAlert:self finishedWithChoice:choice]; +} + +- (IBAction)installUpdate:sender +{ + [self endWithSelection:SUInstallUpdateChoice]; +} + +- (IBAction)skipThisVersion:sender +{ + [self endWithSelection:SUSkipThisVersionChoice]; +} + +- (IBAction)remindMeLater:sender +{ + [self endWithSelection:SURemindMeLaterChoice]; +} + +- (void)displayReleaseNotes +{ + [releaseNotesView setFrameLoadDelegate:self]; + [releaseNotesView setPolicyDelegate:self]; + + // Stick a nice big spinner in the middle of the web view until the page is loaded. + NSRect frame = [[releaseNotesView superview] frame]; + releaseNotesSpinner = [[[NSProgressIndicator alloc] initWithFrame:NSMakeRect(NSMidX(frame)-16, NSMidY(frame)-16, 32, 32)] autorelease]; + [releaseNotesSpinner setStyle:NSProgressIndicatorSpinningStyle]; + [releaseNotesSpinner startAnimation:self]; + webViewFinishedLoading = NO; + [[releaseNotesView superview] addSubview:releaseNotesSpinner]; + + // If there's a release notes URL, load it; otherwise, just stick the contents of the description into the web view. + if ([updateItem releaseNotesURL]) + { + [[releaseNotesView mainFrame] loadRequest:[NSURLRequest requestWithURL:[updateItem releaseNotesURL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30]]; + } + else + { + [[releaseNotesView mainFrame] loadHTMLString:[updateItem description] baseURL:nil]; + } +} + +- (BOOL)showsReleaseNotes +{ + if (!SUInfoValueForKey(SUShowReleaseNotesKey)) { return YES; } // defaults to YES + return [SUInfoValueForKey(SUShowReleaseNotesKey) boolValue]; +} + +- (BOOL)allowsAutomaticUpdates +{ + if (!SUInfoValueForKey(SUExpectsDSASignatureKey)) { return NO; } // automatic updating requires DSA-signed updates + if (!SUInfoValueForKey(SUAllowsAutomaticUpdatesKey)) { return YES; } // defaults to YES + return [SUInfoValueForKey(SUAllowsAutomaticUpdatesKey) boolValue]; +} + +- (void)awakeFromNib +{ + [[self window] setLevel:NSFloatingWindowLevel]; + [[self window] setFrameAutosaveName:@"SUUpdateAlertFrame"]; + + // We're gonna do some frame magic to match the window's size to the description field and the presence of the release notes view. + NSRect frame = [[self window] frame]; + + if (![self showsReleaseNotes]) + { + // Resize the window to be appropriate for not having a huge release notes view. + frame.size.height -= [releaseNotesView frame].size.height; + // No resizing! + [[self window] setShowsResizeIndicator:NO]; + [[self window] setMinSize:frame.size]; + [[self window] setMaxSize:frame.size]; + } + + if (![self allowsAutomaticUpdates]) + { + NSRect boxFrame = [[[releaseNotesView superview] superview] frame]; + boxFrame.origin.y -= 20; + boxFrame.size.height += 20; + [[[releaseNotesView superview] superview] setFrame:boxFrame]; + } + + [[self window] setFrame:frame display:NO]; + [[self window] center]; + + if ([self showsReleaseNotes]) + { + [self displayReleaseNotes]; + } +} + +- (BOOL)windowShouldClose:note +{ + [self endWithSelection:SURemindMeLaterChoice]; + return YES; +} + +- (NSImage *)applicationIcon +{ + return [NSImage imageNamed:@"NSApplicationIcon"]; +} + +- (NSString *)titleText +{ + return [NSString stringWithFormat:SULocalizedString(@"A new version of %@ is available!", nil), SUHostAppDisplayName()]; +} + +- (NSString *)descriptionText +{ + return [NSString stringWithFormat:SULocalizedString(@"%@ %@ is now available (you have %@). Would you like to download it now?", nil), SUHostAppDisplayName(), [updateItem versionString], SUHostAppVersionString()]; +} + +- (void)webView:(WebView *)sender didFinishLoadForFrame:frame +{ + if ([frame parentFrame] == nil) { + webViewFinishedLoading = YES; + [releaseNotesSpinner setHidden:YES]; + [sender display]; // necessary to prevent weird scroll bar artifacting + } +} + +- (void)webView:sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:frame decisionListener:listener +{ + if (webViewFinishedLoading == YES) { + [[NSWorkspace sharedWorkspace] openURL:[request URL]]; + + [listener ignore]; + } + else { + [listener use]; + } +} + +- (void)setDelegate:del +{ + delegate = del; +} + +@end diff --git a/SUUpdater+Authentication.m b/SUUpdater+Authentication.m new file mode 100644 index 000000000..b622abe2c --- /dev/null +++ b/SUUpdater+Authentication.m @@ -0,0 +1,54 @@ +// +// SUUpdater+Authentication.m +// Pixen +// +// Created by Andy Matuschak on 3/9/06. +// Copyright 2006 __MyCompanyName__. All rights reserved. +// + +#import "SUUpdater.h" +#import "sys/stat.h" +#import + +@implementation SUUpdater (SUAuthenticationAdditions) + +// Thanks to Allan Odgaard for this! +- (BOOL)moveFileWithAuthenticationFrom:(NSString *)src toNewPath:(NSString *)dst withTempPath:(NSString *)tmp +{ + BOOL res = NO; + struct stat sb; + if((stat([src UTF8String], &sb) != 0) || (stat([tmp UTF8String], &sb) == 0) || stat([dst UTF8String], &sb) != 0) + return false; + + char* buf = NULL; + asprintf(&buf, + "mv -f \"$DST_PATH\" \"$TMP_PATH\" && " + "mv -f \"$SRC_PATH\" \"$DST_PATH\" && " + "rm -rf \"$TMP_PATH\" && " + "chown -R %d:%d \"$DST_PATH\"", + sb.st_uid, sb.st_gid); + + if(!buf) + return false; + + AuthorizationRef auth; + if(AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth) == errAuthorizationSuccess) + { + setenv("SRC_PATH", [src UTF8String], 1); + setenv("DST_PATH", [dst UTF8String], 1); + setenv("TMP_PATH", [tmp UTF8String], 1); + + char const* arguments[] = { "-c", buf, NULL }; + if(AuthorizationExecuteWithPrivileges(auth, "/bin/sh", kAuthorizationFlagDefaults, (char**)arguments, NULL) == errAuthorizationSuccess) + { + int status; + int pid = wait(&status); + if(pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0) + res = YES; + } + } + free(buf); + return res; +} + +@end diff --git a/SUUpdater+DSA.m b/SUUpdater+DSA.m new file mode 100644 index 000000000..3567176ac --- /dev/null +++ b/SUUpdater+DSA.m @@ -0,0 +1,131 @@ +// +// SUUpdater+DSA.m +// Pixen +// +// Created by Andy Matuschak on 3/9/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +// Thanks to Allan Odgaard for this code! + +#import "SUUpdater.h" +#import +#import +#import +#import +#import +#import + +@implementation SUUpdater (SUDSAAdditions) + +int b64decode(unsigned char* str) +{ + unsigned char *cur, *start; + int d, dlast, phase; + unsigned char c; + static int table[256] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 00-0F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 10-1F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, /* 20-2F */ + 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, /* 30-3F */ + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, /* 40-4F */ + 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, /* 50-5F */ + -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, /* 60-6F */ + 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, /* 70-7F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 80-8F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 90-9F */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* A0-AF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* B0-BF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* C0-CF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* D0-DF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* E0-EF */ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 /* F0-FF */ + }; + + d = dlast = phase = 0; + start = str; + for (cur = str; *cur != '\0'; ++cur ) + { + if(*cur == '\n' || *cur == '\r'){phase = dlast = 0; continue;} + d = table[(int)*cur]; + if(d != -1) + { + switch(phase) + { + case 0: + ++phase; + break; + case 1: + c = ((dlast << 2) | ((d & 0x30) >> 4)); + *str++ = c; + ++phase; + break; + case 2: + c = (((dlast & 0xf) << 4) | ((d & 0x3c) >> 2)); + *str++ = c; + ++phase; + break; + case 3: + c = (((dlast & 0x03 ) << 6) | d); + *str++ = c; + phase = 0; + break; + } + dlast = d; + } + } + *str = '\0'; + return str - start; +} + +EVP_PKEY* load_dsa_key(char *key) +{ + EVP_PKEY* pkey = NULL; + BIO *bio; + if(bio = BIO_new_mem_buf(key, strlen(key))) + { + DSA* dsa_key = 0; + if(PEM_read_bio_DSA_PUBKEY(bio, &dsa_key, NULL, NULL)) + { + if(pkey = EVP_PKEY_new()) + { + if(EVP_PKEY_assign_DSA(pkey, dsa_key) != 1) + { + DSA_free(dsa_key); + EVP_PKEY_free(pkey); + pkey = NULL; + } + } + } + BIO_free(bio); + } + return pkey; +} + +- (BOOL)validateArchiveAtPath:(NSString *)path withSignature:(NSString *)encodedSignature +{ + EVP_PKEY* pkey; + if(!encodedSignature || ![self infoValueForKey:SUPublicDSAKeyKey] || !(pkey = load_dsa_key((char *)[[self infoValueForKey:SUPublicDSAKeyKey] UTF8String]))) + return NO; + + // Now, the signature is in base64; we have to decode it into a binary stream. + unsigned char *signature = (unsigned char *)[encodedSignature UTF8String]; + long length = b64decode(signature); + + NSData *pathData = [NSData dataWithContentsOfFile:path]; + if (!pathData) { return NO; } + unsigned char md[SHA_DIGEST_LENGTH]; + SHA1([pathData bytes], [pathData length], md); + + BOOL res = false; + EVP_MD_CTX ctx; + if(EVP_VerifyInit(&ctx, EVP_dss1()) == 1) + { + EVP_VerifyUpdate(&ctx, md, SHA_DIGEST_LENGTH); + res = EVP_VerifyFinal(&ctx, signature, length, pkey) == 1; + } + EVP_PKEY_free(pkey); + return res; +} + +@end diff --git a/SUUpdater.h b/SUUpdater.h new file mode 100644 index 000000000..2dc3fd4fe --- /dev/null +++ b/SUUpdater.h @@ -0,0 +1,57 @@ +// +// SUUpdater.h +// Sparkle +// +// Created by Andy Matuschak on 1/4/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +// Before you use Sparkle in your app, you must set SUFeedURL in Info.plist to the +// address of the appcast on your webserver. If you don't already have an +// appcast, please see the Sparkle documentation to learn about how to set one up. + +// .zip, .dmg, .tar, .tbz, .tgz archives are supported at this time. + +// By default, Sparkle offers to show the user the release notes of the build they'll be +// getting, which it assumes are in the description (or body) field of the relevant RSS item. +// Set SUShowReleaseNotes to in Info.plist to hide the button. + +@class SUAppcastItem, SUUpdateAlert, SUStatusController; +@interface SUUpdater : NSObject { + SUAppcastItem *updateItem; + + SUStatusController *statusController; + SUUpdateAlert *updateAlert; + + NSURLDownload *downloader; + NSString *downloadPath; + + NSTimer *checkTimer; + NSTimeInterval checkInterval; + + BOOL verbose; + BOOL updateInProgress; + + NSString *currentSystemVersion; +} + +// This IBAction is meant for a main menu item. Hook up any menu item to this action, +// and Sparkle will check for updates and report back its findings verbosely. +- (IBAction)checkForUpdates:sender; + +// This method is similar to the above, but it's intended for updates initiated by +// the computer instead of by the user. It does not alert the user when he is up to date, +// and it remains silent about network errors in fetching the feed. This is what you +// want to call to update programmatically; only use checkForUpdates: with buttons and menu items. +- (void)checkForUpdatesInBackground; + +// This method allows you to schedule a check to run every time interval. You can +// pass 0 to this method to cancel a previously scheduled timer. You probably don't want +// to call this directly: if you set a SUScheduledCheckInterval key in Info.plist or +// the user defaults, Sparkle will set this up for you automatically on startup. You might +// just want to call this every time the user changes the setting in the preferences. +- (void)scheduleCheckWithInterval:(NSTimeInterval)interval; + +@end diff --git a/SUUpdater.m b/SUUpdater.m new file mode 100644 index 000000000..3caaf55ff --- /dev/null +++ b/SUUpdater.m @@ -0,0 +1,603 @@ +// +// SUUpdater.m +// Sparkle +// +// Created by Andy Matuschak on 1/4/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUUpdater.h" +#import "SUAppcast.h" +#import "SUAppcastItem.h" +#import "SUUnarchiver.h" +#import "SUUtilities.h" + +#import "SUUpdateAlert.h" +#import "SUAutomaticUpdateAlert.h" +#import "SUStatusController.h" + +#import "NSFileManager+Authentication.h" +#import "NSFileManager+Verification.h" +#import "NSApplication+AppCopies.h" + +#import +#import +#import +#import +#import + +@interface SUUpdater (Private) +- (void)checkForUpdatesAndNotify:(BOOL)verbosity; +- (void)showUpdateErrorAlertWithInfo:(NSString *)info; +- (NSTimeInterval)storedCheckInterval; +- (void)abandonUpdate; +- (IBAction)installAndRestart:sender; +- (NSString *)systemVersionString; +@end + +@implementation SUUpdater + +- init +{ + [super init]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:@"NSApplicationDidFinishLaunchingNotification" object:NSApp]; + + // OS version (Apple recommends using SystemVersion.plist instead of Gestalt() here, don't ask me why). + // This code *should* use NSSearchPathForDirectoriesInDomains(NSCoreServiceDirectory, NSSystemDomainMask, YES) + // but that returns /Library/CoreServices for some reason + NSString *versionPlistPath = @"/System/Library/CoreServices/SystemVersion.plist"; + //gets a version string of the form X.Y.Z + currentSystemVersion = [[[NSDictionary dictionaryWithContentsOfFile:versionPlistPath] objectForKey:@"ProductVersion"] retain]; + return self; +} + +- (void)scheduleCheckWithInterval:(NSTimeInterval)interval +{ + if (checkTimer) + { + [checkTimer invalidate]; + checkTimer = nil; + } + + checkInterval = interval; + if (interval > 0) + checkTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:YES]; +} + +- (void)scheduleCheckWithIntervalObject:(NSNumber *)interval +{ + [self scheduleCheckWithInterval:[interval doubleValue]]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)note +{ + // If there's a scheduled interval, we see if it's been longer than that interval since the last + // check. If so, we perform a startup check; if not, we don't. + if ([self storedCheckInterval]) + { + NSTimeInterval interval = [self storedCheckInterval]; + NSDate *lastCheck = [[NSUserDefaults standardUserDefaults] objectForKey:SULastCheckTimeKey]; + if (!lastCheck) { lastCheck = [NSDate date]; } + NSTimeInterval intervalSinceCheck = [[NSDate date] timeIntervalSinceDate:lastCheck]; + if (intervalSinceCheck < interval) + { + // Hasn't been long enough; schedule a check for the future. + [self performSelector:@selector(checkForUpdatesInBackground) withObject:nil afterDelay:(interval-intervalSinceCheck)]; + [self performSelector:@selector(scheduleCheckWithIntervalObject:) withObject:[NSNumber numberWithLong:interval] afterDelay:(interval-intervalSinceCheck)]; + } + else + { + [self scheduleCheckWithInterval:interval]; + [self checkForUpdatesInBackground]; + } + } + else + { + // There's no scheduled check, so let's see if we're supposed to check on startup. + NSNumber *shouldCheckAtStartup = [[NSUserDefaults standardUserDefaults] objectForKey:SUCheckAtStartupKey]; + if (!shouldCheckAtStartup) // hasn't been set yet; ask the user + { + // Let's see if there's a key in Info.plist for a default, though. We'll let that override the dialog if it's there. + NSNumber *infoStartupValue = SUInfoValueForKey(SUCheckAtStartupKey); + if (infoStartupValue) + { + shouldCheckAtStartup = infoStartupValue; + } + else + { + shouldCheckAtStartup = [NSNumber numberWithBool:NSRunAlertPanel(SULocalizedString(@"Check for updates on startup?", nil), [NSString stringWithFormat:SULocalizedString(@"Would you like %@ to check for updates on startup? If not, you can initiate the check manually from the application menu.", nil), SUHostAppDisplayName()], SULocalizedString(@"Yes", nil), SULocalizedString(@"No", nil), nil) == NSAlertDefaultReturn]; + } + [[NSUserDefaults standardUserDefaults] setObject:shouldCheckAtStartup forKey:SUCheckAtStartupKey]; + } + + if ([shouldCheckAtStartup boolValue]) + [self checkForUpdatesInBackground]; + } +} + +- (void)dealloc +{ + [updateItem release]; + [updateAlert release]; + + [downloadPath release]; + [statusController release]; + [downloader release]; + + if (checkTimer) + [checkTimer invalidate]; + + if (currentSystemVersion) + [currentSystemVersion release]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)checkForUpdatesInBackground +{ + [self checkForUpdatesAndNotify:NO]; +} + +- (IBAction)checkForUpdates:sender +{ + [self checkForUpdatesAndNotify:YES]; // if we're coming from IB, then we want to be more verbose. +} + +// If the verbosity flag is YES, Sparkle will say when it can't reach the server and when there's no new update. +// This is generally useful for a menu item--when the check is explicitly invoked. +- (void)checkForUpdatesAndNotify:(BOOL)verbosity +{ + if (updateInProgress) + { + if (verbosity) + { + NSBeep(); + if ([[statusController window] isVisible]) + [statusController showWindow:self]; + else if ([[updateAlert window] isVisible]) + [updateAlert showWindow:self]; + else + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An update is already in progress!", nil)]; + } + return; + } + verbose = verbosity; + updateInProgress = YES; + + // 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 = [[NSUserDefaults standardUserDefaults] objectForKey:SUFeedURLKey]; + if (!appcastString) + appcastString = SUInfoValueForKey(SUFeedURLKey); + if (!appcastString) { [NSException raise:@"SUNoFeedURL" format:@"No feed URL is specified in the Info.plist or the user defaults!"]; } + + SUAppcast *appcast = [[SUAppcast alloc] init]; + [appcast setDelegate:self]; + [appcast fetchAppcastFromURL:[NSURL URLWithString:appcastString]]; +} + +- (BOOL)automaticallyUpdates +{ + // If the SUAllowsAutomaticUpdatesKey exists and is set to NO, return NO. + if ([SUInfoValueForKey(SUAllowsAutomaticUpdatesKey) boolValue] == NO && SUInfoValueForKey(SUAllowsAutomaticUpdatesKey)) { return NO; } + + // If we're not using DSA signatures, we aren't going to trust any updates automatically. + if (![SUInfoValueForKey(SUExpectsDSASignatureKey) boolValue]) { return NO; } + + // If there's no setting, we default to NO. + if (![[NSUserDefaults standardUserDefaults] objectForKey:SUAutomaticallyUpdateKey]) { return NO; } + + return [[[NSUserDefaults standardUserDefaults] objectForKey:SUAutomaticallyUpdateKey] boolValue]; +} + +- (BOOL)isAutomaticallyUpdating +{ + return [self automaticallyUpdates] && !verbose; +} + +- (void)showUpdateErrorAlertWithInfo:(NSString *)info +{ + if ([self isAutomaticallyUpdating]) { return; } + NSRunAlertPanel(SULocalizedString(@"Update Error!", nil), info, SULocalizedString(@"Cancel", nil), nil, nil); +} + +- (NSTimeInterval)storedCheckInterval +{ + // Returns the scheduled check interval stored in the user defaults / info.plist. User defaults override Info.plist. + if ([[NSUserDefaults standardUserDefaults] objectForKey:SUScheduledCheckIntervalKey]) + { + long interval = [[[NSUserDefaults standardUserDefaults] objectForKey:SUScheduledCheckIntervalKey] longValue]; + if (interval > 0) + return interval; + } + if (SUInfoValueForKey(SUScheduledCheckIntervalKey)) + return [SUInfoValueForKey(SUScheduledCheckIntervalKey) longValue]; + return 0; +} + +- (void)beginDownload +{ + if (![self isAutomaticallyUpdating]) + { + statusController = [[SUStatusController alloc] init]; + [statusController beginActionWithTitle:SULocalizedString(@"Downloading update...", nil) maxProgressValue:0 statusText:nil]; + [statusController setButtonTitle:SULocalizedString(@"Cancel", nil) target:self action:@selector(cancelDownload:) isDefault:NO]; + [statusController showWindow:self]; + } + + downloader = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self]; +} + +- (void)remindMeLater +{ + // Clear out the skipped version so the dialog will actually come back if it was already skipped. + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:SUSkippedVersionKey]; + + if (checkInterval) + [self scheduleCheckWithInterval:checkInterval]; + else + { + // If the host hasn't provided a check interval, we'll use 30 minutes. + [self scheduleCheckWithInterval:30 * 60]; + } +} + +- (void)updateAlert:(SUUpdateAlert *)alert finishedWithChoice:(SUUpdateAlertChoice)choice +{ + [alert release]; + switch (choice) + { + case SUInstallUpdateChoice: + // Clear out the skipped version so the dialog will come back if the download fails. + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:SUSkippedVersionKey]; + [self beginDownload]; + break; + + case SURemindMeLaterChoice: + updateInProgress = NO; + [self remindMeLater]; + break; + + case SUSkipThisVersionChoice: + updateInProgress = NO; + [[NSUserDefaults standardUserDefaults] setObject:[updateItem fileVersion] forKey:SUSkippedVersionKey]; + break; + } +} + +- (void)showUpdatePanel +{ + updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem]; + [updateAlert setDelegate:self]; + + // Only show the update alert if the app is active; otherwise, we'll wait until it is. + if ([NSApp isActive]) + [updateAlert showWindow:self]; + else + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"NSApplicationDidBecomeActiveNotification" object:NSApp]; +} + +- (void)appcastDidFailToLoad:(SUAppcast *)ac +{ + [ac autorelease]; + updateInProgress = NO; + if (verbose) + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)]; +} + +// Override this to change the new version comparison logic! +- (BOOL)newVersionAvailable +{ + BOOL canRunOnCurrentSystem = SUStandardVersionComparison([updateItem minimumSystemVersion], [self systemVersionString]); + return (canRunOnCurrentSystem && (SUStandardVersionComparison([updateItem fileVersion], SUHostAppVersion()) == NSOrderedAscending)); + // Want straight-up string comparison like Sparkle 1.0b3 and earlier? Uncomment the line below and comment the one above. + // return ![SUHostAppVersion() isEqualToString:[updateItem fileVersion]]; +} + +- (NSString *)systemVersionString +{ + return currentSystemVersion; +} + +- (void)appcastDidFinishLoading:(SUAppcast *)ac +{ + @try + { + if (!ac) { [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."]; } + + updateItem = [[ac newestItem] retain]; + [ac autorelease]; + + // Record the time of the check for host app use and for interval checks on startup. + [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:SULastCheckTimeKey]; + + if (![updateItem fileVersion]) + { + [NSException raise:@"SUAppcastException" format:@"Can't extract a version string from the appcast feed. The filenames should look like YourApp_1.5.tgz, where 1.5 is the version number."]; + } + + if (!verbose && [[[NSUserDefaults standardUserDefaults] objectForKey:SUSkippedVersionKey] isEqualToString:[updateItem fileVersion]]) { updateInProgress = NO; return; } + + if ([self newVersionAvailable]) + { + if (checkTimer) // There's a new version! Let's disable the automated checking timer unless the user cancels. + { + [checkTimer invalidate]; + checkTimer = nil; + } + + if ([self isAutomaticallyUpdating]) + { + [self beginDownload]; + } + else + { + [self showUpdatePanel]; + } + } + else + { + if (verbose) // We only notify on no new version when we're being verbose. + { + NSRunAlertPanel(SULocalizedString(@"You're up to date!", nil), [NSString stringWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), SUHostAppDisplayName(), SUHostAppVersionString()], SULocalizedString(@"OK", nil), nil, nil); + } + updateInProgress = NO; + } + } + @catch (NSException *e) + { + NSLog([e reason]); + updateInProgress = NO; + if (verbose) + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)]; + } +} + +- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response +{ + [statusController setMaxProgressValue:[response expectedContentLength]]; +} + +- (void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)name +{ + // If name ends in .txt, the server probably has a stupid MIME configuration. We'll give + // the developer the benefit of the doubt and chop that off. + if ([[name pathExtension] isEqualToString:@"txt"]) + name = [name stringByDeletingPathExtension]; + + // We create a temporary directory in /tmp and stick the file there. + NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; + BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil]; + if (!success) + { + [NSException raise:@"SUFailTmpWrite" format:@"Couldn't create temporary directory in /var/tmp"]; + [download cancel]; + [download release]; + } + + [downloadPath autorelease]; + downloadPath = [[tempDir stringByAppendingPathComponent:name] retain]; + [download setDestination:downloadPath allowOverwrite:YES]; +} + +- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length +{ + [statusController setProgressValue:[statusController progressValue] + length]; + [statusController setStatusText:[NSString stringWithFormat:SULocalizedString(@"%.0lfk of %.0lfk", nil), [statusController progressValue] / 1024.0, [statusController maxProgressValue] / 1024.0]]; +} + +- (void)unarchiver:(SUUnarchiver *)ua extractedLength:(long)length +{ + if ([self isAutomaticallyUpdating]) { return; } + if ([statusController maxProgressValue] == 0) + [statusController setMaxProgressValue:[[[[NSFileManager defaultManager] fileAttributesAtPath:downloadPath traverseLink:NO] objectForKey:NSFileSize] longValue]]; + [statusController setProgressValue:[statusController progressValue] + length]; +} + +- (void)unarchiverDidFinish:(SUUnarchiver *)ua +{ + [ua autorelease]; + + if ([self isAutomaticallyUpdating]) + { + [self installAndRestart:self]; + } + else + { + [statusController beginActionWithTitle:SULocalizedString(@"Ready to install!", nil) maxProgressValue:1 statusText:nil]; + [statusController setProgressValue:1]; // fill the bar + [statusController setButtonTitle:SULocalizedString(@"Install and Relaunch", nil) target:self action:@selector(installAndRestart:) isDefault:YES]; + [NSApp requestUserAttention:NSInformationalRequest]; + } +} + +- (void)unarchiverDidFail:(SUUnarchiver *)ua +{ + [ua autorelease]; + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil)]; + [self abandonUpdate]; +} + +- (void)extractUpdate +{ + // Now we have to extract the downloaded archive. + if (![self isAutomaticallyUpdating]) + [statusController beginActionWithTitle:SULocalizedString(@"Extracting update...", nil) maxProgressValue:0 statusText:nil]; + + @try + { + // If the developer's provided a sparkle:md5Hash attribute on the enclosure, let's verify that. + if ([updateItem MD5Sum] && ![[NSFileManager defaultManager] validatePath:downloadPath withMD5Hash:[updateItem MD5Sum]]) + { + [NSException raise:@"SUUnarchiveException" format:@"MD5 verification of the update archive failed."]; + } + + // DSA verification, if activated by the developer + if ([SUInfoValueForKey(SUExpectsDSASignatureKey) boolValue]) + { + NSString *dsaSignature = [updateItem DSASignature]; + if (![[NSFileManager defaultManager] validatePath:downloadPath withEncodedDSASignature:dsaSignature]) + { + [NSException raise:@"SUUnarchiveException" format:@"DSA verification of the update archive failed."]; + } + } + + SUUnarchiver *unarchiver = [[SUUnarchiver alloc] init]; + [unarchiver setDelegate:self]; + [unarchiver unarchivePath:downloadPath]; // asynchronous extraction! + } + @catch(NSException *e) { + NSLog([e reason]); + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil)]; + [self abandonUpdate]; + } +} + +- (void)downloadDidFinish:(NSURLDownload *)download +{ + [download release]; + downloader = nil; + [self extractUpdate]; +} + +- (void)abandonUpdate +{ + [updateItem autorelease]; + updateItem = nil; + [statusController close]; + [statusController autorelease]; + statusController = nil; + updateInProgress = NO; +} + +- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error +{ + [self abandonUpdate]; + + NSLog(@"Download error: %@", [error localizedDescription]); + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while trying to download the file. Please try again later.", nil)]; +} + +- (IBAction)installAndRestart:sender +{ + NSString *currentAppPath = [[NSBundle mainBundle] bundlePath]; + NSString *newAppDownloadPath = [[downloadPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:[SUUnlocalizedInfoValueForKey(@"CFBundleName") stringByAppendingPathExtension:@"app"]]; + @try + { + if (![self isAutomaticallyUpdating]) + { + [statusController beginActionWithTitle:SULocalizedString(@"Installing update...", nil) maxProgressValue:0 statusText:nil]; + [statusController setButtonEnabled:NO]; + + // We have to wait for the UI to update. + NSEvent *event; + while((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])) + [NSApp sendEvent:event]; + } + + // We assume that the archive will contain a file named {CFBundleName}.app + // (where, obviously, CFBundleName comes from Info.plist) + if (!SUUnlocalizedInfoValueForKey(@"CFBundleName")) { [NSException raise:@"SUInstallException" format:@"This application has no CFBundleName! This key must be set to the application's name."]; } + + // Search subdirectories for the application + NSString *file, *appName = [SUUnlocalizedInfoValueForKey(@"CFBundleName") stringByAppendingPathExtension:@"app"]; + NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:[downloadPath stringByDeletingLastPathComponent]]; + while ((file = [dirEnum nextObject])) + { + // Some DMGs have symlinks into /Applications! That's no good! + if ([file isEqualToString:@"/Applications"]) + [dirEnum skipDescendents]; + if ([[file lastPathComponent] isEqualToString:appName]) // We found one! + { + newAppDownloadPath = [[downloadPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:file]; + break; + } + if ([[file pathExtension] isEqualToString:@".app"]) // No point in looking in app bundles. + [dirEnum skipDescendents]; + } + + if (!newAppDownloadPath || ![[NSFileManager defaultManager] fileExistsAtPath:newAppDownloadPath]) + { + [NSException raise:@"SUInstallException" format:@"The update archive didn't contain an application with the proper name: %@. Remember, the updated app's file name must be identical to {CFBundleName}.app", [SUInfoValueForKey(@"CFBundleName") stringByAppendingPathExtension:@"app"]]; + } + } + @catch(NSException *e) + { + NSLog([e reason]); + [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred during installation. Please try again later.", nil)]; + [self abandonUpdate]; + return; + } + + if ([self isAutomaticallyUpdating]) // Don't do authentication if we're automatically updating; that'd be surprising. + { + int tag = 0; + BOOL result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[currentAppPath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[currentAppPath lastPathComponent]] tag:&tag]; + result &= [[NSFileManager defaultManager] movePath:newAppDownloadPath toPath:currentAppPath handler:nil]; + if (!result) + { + [self abandonUpdate]; + return; + } + } + else // But if we're updating by the action of the user, do an authenticated move. + { + // Outside of the @try block because we want to be a little more informative on this error. + if (![[NSFileManager defaultManager] movePathWithAuthentication:newAppDownloadPath toPath:currentAppPath]) + { + [self showUpdateErrorAlertWithInfo:[NSString stringWithFormat:SULocalizedString(@"%@ does not have permission to write to the application's directory! Are you running off a disk image? If not, ask your system administrator for help.", nil), SUHostAppDisplayName()]]; + [self abandonUpdate]; + return; + } + } + + // Prompt for permission to restart if we're automatically updating. + if ([self isAutomaticallyUpdating]) + { + SUAutomaticUpdateAlert *alert = [[SUAutomaticUpdateAlert alloc] initWithAppcastItem:updateItem]; + if ([NSApp runModalForWindow:[alert window]] == NSAlertAlternateReturn) + { + [alert release]; + return; + } + } + + [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self]; + + // Thanks to Allan Odgaard for this restart code, which is much more clever than mine was. + setenv("LAUNCH_PATH", [currentAppPath UTF8String], 1); + setenv("TEMP_FOLDER", [[downloadPath stringByDeletingLastPathComponent] UTF8String], 1); // delete the temp stuff after it's all over + system("/bin/bash -c '{ for (( i = 0; i < 3000 && $(echo $(/bin/ps -xp $PPID|/usr/bin/wc -l))-1; i++ )); do\n" + " /bin/sleep .2;\n" + " done\n" + " if [[ $(/bin/ps -xp $PPID|/usr/bin/wc -l) -ne 2 ]]; then\n" + " /usr/bin/open \"${LAUNCH_PATH}\"\n" + " fi\n" + " rm -rf \"${TEMP_FOLDER}\"\n" + "} &>/dev/null &'"); + [NSApp terminate:self]; +} + +- (IBAction)cancelDownload:sender +{ + if (downloader) + { + [downloader cancel]; + [downloader release]; + } + [self abandonUpdate]; + + if (checkInterval) + { + [self scheduleCheckWithInterval:checkInterval]; + } +} + +- (void)applicationDidBecomeActive:(NSNotification *)aNotification +{ + // We don't want to display the update alert until the application becomes active. + [updateAlert showWindow:self]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"NSApplicationDidBecomeActiveNotification" object:NSApp]; +} + +@end diff --git a/SUUtilities.h b/SUUtilities.h new file mode 100644 index 000000000..18a948dfc --- /dev/null +++ b/SUUtilities.h @@ -0,0 +1,21 @@ +// +// SUUtilities.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +id SUInfoValueForKey(NSString *key); +id SUUnlocalizedInfoValueForKey(NSString *key); +NSString *SUHostAppName(); +NSString *SUHostAppDisplayName(); +NSString *SUHostAppVersion(); +NSString *SUHostAppVersionString(); + +NSComparisonResult SUStandardVersionComparison(NSString * versionA, NSString * versionB); + +// If running make localizable-strings for genstrings, ignore the error on this line. +NSString *SULocalizedString(NSString *key, NSString *comment); diff --git a/SUUtilities.m b/SUUtilities.m new file mode 100644 index 000000000..4ef517749 --- /dev/null +++ b/SUUtilities.m @@ -0,0 +1,207 @@ +// +// SUUtilities.m +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUUtilities.h" + +@interface SUUtilities : NSObject + +(NSString *)localizedStringForKey:(NSString *)key withComment:(NSString *)comment; +@end + +id SUUnlocalizedInfoValueForKey(NSString *key) +{ + // Okay, but if it isn't there, let's use the general one. + id value = [[[NSBundle mainBundle] infoDictionary] valueForKey:key]; + if (!value) + return SUInfoValueForKey(key); + return value; +} + +id SUInfoValueForKey(NSString *key) +{ + return [[NSBundle mainBundle] objectForInfoDictionaryKey:key]; +} + +NSString *SUHostAppName() +{ + if (SUInfoValueForKey(@"CFBundleName")) { return SUInfoValueForKey(@"CFBundleName"); } + return [[[NSFileManager defaultManager] displayNameAtPath:[[NSBundle mainBundle] bundlePath]] stringByDeletingPathExtension]; +} + +NSString *SUHostAppDisplayName() +{ + if (SUInfoValueForKey(@"CFBundleDisplayName")) { return SUInfoValueForKey(@"CFBundleDisplayName"); } + return SUHostAppName(); +} + +NSString *SUHostAppVersion() +{ + return SUInfoValueForKey(@"CFBundleVersion"); +} + +NSString *SUHostAppVersionString() +{ + NSString *shortVersionString = SUInfoValueForKey(@"CFBundleShortVersionString"); + if (shortVersionString) + { + if (![shortVersionString isEqualToString:SUHostAppVersion()]) + shortVersionString = [shortVersionString stringByAppendingFormat:@"/%@", SUHostAppVersion()]; + return shortVersionString; + } + else + return SUHostAppVersion(); // fall back on CFBundleVersion +} + +NSString *SULocalizedString(NSString *key, NSString *comment) { + return [SUUtilities localizedStringForKey:key withComment:comment]; +} + +enum { + kNumberType, + kStringType, + kPeriodType +}; + +// The version comparison code here is courtesy of Kevin Ballard, adapted from MacPAD. Thanks, Kevin! + +int SUGetCharType(NSString *character) +{ + if ([character isEqualToString:@"."]) { + return kPeriodType; + } else if ([character isEqualToString:@"0"] || [character intValue] != 0) { + return kNumberType; + } else { + return kStringType; + } +} + +NSArray *SUSplitVersionString(NSString *version) +{ + NSString *character; + NSMutableString *s; + int i, n, oldType, newType; + NSMutableArray *parts = [NSMutableArray array]; + if ([version length] == 0) { + // Nothing to do here + return parts; + } + s = [[[version substringToIndex:1] mutableCopy] autorelease]; + oldType = SUGetCharType(s); + n = [version length] - 1; + for (i = 1; i <= n; ++i) { + character = [version substringWithRange:NSMakeRange(i, 1)]; + newType = SUGetCharType(character); + if (oldType != newType || oldType == kPeriodType) { + // We've reached a new segment + NSString *aPart = [[NSString alloc] initWithString:s]; + [parts addObject:aPart]; + [aPart release]; + [s setString:character]; + } else { + // Add character to string and continue + [s appendString:character]; + } + oldType = newType; + } + + // Add the last part onto the array + [parts addObject:[NSString stringWithString:s]]; + return parts; +} + +NSComparisonResult SUStandardVersionComparison(NSString *versionA, NSString *versionB) +{ + NSArray *partsA = SUSplitVersionString(versionA); + NSArray *partsB = SUSplitVersionString(versionB); + + NSString *partA, *partB; + int i, n, typeA, typeB, intA, intB; + + n = MIN([partsA count], [partsB count]); + for (i = 0; i < n; ++i) { + partA = [partsA objectAtIndex:i]; + partB = [partsB objectAtIndex:i]; + + typeA = SUGetCharType(partA); + typeB = SUGetCharType(partB); + + // Compare types + if (typeA == typeB) { + // Same type; we can compare + if (typeA == kNumberType) { + intA = [partA intValue]; + intB = [partB intValue]; + if (intA > intB) { + return NSOrderedAscending; + } else if (intA < intB) { + return NSOrderedDescending; + } + } else if (typeA == kStringType) { + NSComparisonResult result = [partB compare:partA]; + if (result != NSOrderedSame) { + return result; + } + } + } else { + // Not the same type? Now we have to do some validity checking + if (typeA != kStringType && typeB == kStringType) { + // typeA wins + return NSOrderedAscending; + } else if (typeA == kStringType && typeB != kStringType) { + // typeB wins + return NSOrderedDescending; + } else { + // One is a number and the other is a period. The period is invalid + if (typeA == kNumberType) { + return NSOrderedAscending; + } else { + return NSOrderedDescending; + } + } + } + } + // The versions are equal up to the point where they both still have parts + // Lets check to see if one is larger than the other + if ([partsA count] != [partsB count]) { + // Yep. Lets get the next part of the larger + // n holds the value we want + NSString *missingPart; + int missingType, shorterResult, largerResult; + + if ([partsA count] > [partsB count]) { + missingPart = [partsA objectAtIndex:n]; + shorterResult = NSOrderedDescending; + largerResult = NSOrderedAscending; + } else { + missingPart = [partsB objectAtIndex:n]; + shorterResult = NSOrderedAscending; + largerResult = NSOrderedDescending; + } + + missingType = SUGetCharType(missingPart); + // Check the type + if (missingType == kStringType) { + // It's a string. Shorter version wins + return shorterResult; + } else { + // It's a number/period. Larger version wins + return largerResult; + } + } + + // The 2 strings are identical + return NSOrderedSame; +} + +@implementation SUUtilities + ++ (NSString *)localizedStringForKey:(NSString *)key withComment:(NSString *)comment +{ + return NSLocalizedStringFromTableInBundle(key, @"Sparkle", [NSBundle bundleForClass:[self class]], comment); +} + +@end diff --git a/Sparkle.h b/Sparkle.h new file mode 100644 index 000000000..36ffd873a --- /dev/null +++ b/Sparkle.h @@ -0,0 +1,22 @@ +// +// Sparkle.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import "SUUpdater.h" +#import "SUUtilities.h" +#import "SUConstants.h" +#import "SUAppcast.h" +#import "SUAppcastItem.h" +#import "SUUpdateAlert.h" +#import "SUAutomaticUpdateAlert.h" +#import "SUStatusController.h" +#import "SUUnarchiver.h" +#import "SUStatusChecker.h" + +#import "NSApplication+AppCopies.h" +#import "NSFileManager+Authentication.h" +#import "NSFileManager+Verification.h" diff --git a/Sparkle.icns b/Sparkle.icns new file mode 100644 index 000000000..8e56d45c0 Binary files /dev/null and b/Sparkle.icns differ diff --git a/Sparkle_Prefix.pch b/Sparkle_Prefix.pch new file mode 100644 index 000000000..759d046c8 --- /dev/null +++ b/Sparkle_Prefix.pch @@ -0,0 +1,8 @@ +// +// Prefix header for all source files of the 'Sparkle' target in the 'Sparkle' project. +// + +#ifdef __OBJC__ + #import + #import "SUConstants.h" +#endif \ No newline at end of file diff --git a/md5.c b/md5.c new file mode 100644 index 000000000..c35d96c5e --- /dev/null +++ b/md5.c @@ -0,0 +1,381 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((data - (const md5_byte_t *)0) & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/md5.h b/md5.h new file mode 100644 index 000000000..698c995d8 --- /dev/null +++ b/md5.h @@ -0,0 +1,91 @@ +/* + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke . + 1999-05-03 lpd Original version. + */ + +#ifndef md5_INCLUDED +# define md5_INCLUDED + +/* + * This package supports both compile-time and run-time determination of CPU + * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be + * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is + * defined as non-zero, the code will be compiled to run only on big-endian + * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to + * run on either big- or little-endian CPUs, but will run slightly less + * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. + */ + +typedef unsigned char md5_byte_t; /* 8-bit byte */ +typedef unsigned int md5_word_t; /* 32-bit word */ + +/* Define the state of the MD5 Algorithm. */ +typedef struct md5_state_s { + md5_word_t count[2]; /* message length in bits, lsw first */ + md5_word_t abcd[4]; /* digest buffer */ + md5_byte_t buf[64]; /* accumulate block */ +} md5_state_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Initialize the algorithm. */ +void md5_init(md5_state_t *pms); + +/* Append a string to the message. */ +void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); + +/* Finish the message and return the digest. */ +void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif + +#endif /* md5_INCLUDED */