| 1 | // |
|---|
| 2 | // SUAppcast.m |
|---|
| 3 | // Sparkle |
|---|
| 4 | // |
|---|
| 5 | // Created by Andy Matuschak on 3/12/06. |
|---|
| 6 | // Copyright 2006 Andy Matuschak. All rights reserved. |
|---|
| 7 | // |
|---|
| 8 | |
|---|
| 9 | #import "Sparkle.h" |
|---|
| 10 | #import "SUAppcast.h" |
|---|
| 11 | |
|---|
| 12 | @interface SUAppcast (Private) |
|---|
| 13 | - (void)reportError:(NSError *)error; |
|---|
| 14 | - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes; |
|---|
| 15 | @end |
|---|
| 16 | |
|---|
| 17 | @implementation SUAppcast |
|---|
| 18 | |
|---|
| 19 | - (void)dealloc |
|---|
| 20 | { |
|---|
| 21 | [items release]; |
|---|
| 22 | [userAgentString release]; |
|---|
| 23 | [super dealloc]; |
|---|
| 24 | } |
|---|
| 25 | |
|---|
| 26 | - (NSArray *)items |
|---|
| 27 | { |
|---|
| 28 | return items; |
|---|
| 29 | } |
|---|
| 30 | |
|---|
| 31 | - (void)fetchAppcastFromURL:(NSURL *)url |
|---|
| 32 | { |
|---|
| 33 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30.0]; |
|---|
| 34 | if (userAgentString) |
|---|
| 35 | [request setValue:userAgentString forHTTPHeaderField:@"User-Agent"]; |
|---|
| 36 | |
|---|
| 37 | NSURLDownload *download = [[[NSURLDownload alloc] initWithRequest:request delegate:self] autorelease]; |
|---|
| 38 | CFRetain(download); |
|---|
| 39 | } |
|---|
| 40 | |
|---|
| 41 | - (void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)filename |
|---|
| 42 | { |
|---|
| 43 | NSString *destinationFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; |
|---|
| 44 | [download setDestination:destinationFilename allowOverwrite:NO]; |
|---|
| 45 | } |
|---|
| 46 | |
|---|
| 47 | - (void)download:(NSURLDownload *)download didCreateDestination:(NSString *)path |
|---|
| 48 | { |
|---|
| 49 | [downloadFilename release]; |
|---|
| 50 | downloadFilename = [path copy]; |
|---|
| 51 | } |
|---|
| 52 | |
|---|
| 53 | - (void)downloadDidFinish:(NSURLDownload *)download |
|---|
| 54 | { |
|---|
| 55 | CFRelease(download); |
|---|
| 56 | |
|---|
| 57 | NSError *error = nil; |
|---|
| 58 | NSXMLDocument *document = [[NSXMLDocument alloc] initWithContentsOfURL:[NSURL fileURLWithPath:downloadFilename] options:0 error:&error]; |
|---|
| 59 | BOOL failed = NO; |
|---|
| 60 | NSArray *xmlItems = nil; |
|---|
| 61 | NSMutableArray *appcastItems = [NSMutableArray array]; |
|---|
| 62 | |
|---|
| 63 | [[NSFileManager defaultManager] removeFileAtPath:downloadFilename handler:nil]; |
|---|
| 64 | [downloadFilename release]; |
|---|
| 65 | downloadFilename = nil; |
|---|
| 66 | |
|---|
| 67 | if (nil == document) |
|---|
| 68 | { |
|---|
| 69 | failed = YES; |
|---|
| 70 | } |
|---|
| 71 | else |
|---|
| 72 | { |
|---|
| 73 | xmlItems = [document nodesForXPath:@"/rss/channel/item" error:&error]; |
|---|
| 74 | if (nil == xmlItems) |
|---|
| 75 | { |
|---|
| 76 | failed = YES; |
|---|
| 77 | } |
|---|
| 78 | } |
|---|
| 79 | |
|---|
| 80 | if (failed == NO) |
|---|
| 81 | { |
|---|
| 82 | |
|---|
| 83 | NSEnumerator *nodeEnum = [xmlItems objectEnumerator]; |
|---|
| 84 | NSXMLNode *node; |
|---|
| 85 | NSMutableDictionary *nodesDict = [NSMutableDictionary dictionary]; |
|---|
| 86 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; |
|---|
| 87 | |
|---|
| 88 | while (failed == NO && (node = [nodeEnum nextObject])) |
|---|
| 89 | { |
|---|
| 90 | // First, we'll "index" all the first-level children of this appcast item so we can pick them out by language later. |
|---|
| 91 | if ([[node children] count]) |
|---|
| 92 | { |
|---|
| 93 | node = [node childAtIndex:0]; |
|---|
| 94 | while (nil != node) |
|---|
| 95 | { |
|---|
| 96 | NSString *name = [node name]; |
|---|
| 97 | if (name) |
|---|
| 98 | { |
|---|
| 99 | NSMutableArray *nodes = [nodesDict objectForKey:name]; |
|---|
| 100 | if (nodes == nil) |
|---|
| 101 | { |
|---|
| 102 | nodes = [NSMutableArray array]; |
|---|
| 103 | [nodesDict setObject:nodes forKey:name]; |
|---|
| 104 | } |
|---|
| 105 | [nodes addObject:node]; |
|---|
| 106 | } |
|---|
| 107 | node = [node nextSibling]; |
|---|
| 108 | } |
|---|
| 109 | } |
|---|
| 110 | |
|---|
| 111 | NSEnumerator *nameEnum = [nodesDict keyEnumerator]; |
|---|
| 112 | NSString *name; |
|---|
| 113 | while ((name = [nameEnum nextObject])) |
|---|
| 114 | { |
|---|
| 115 | node = [self bestNodeInNodes:[nodesDict objectForKey:name]]; |
|---|
| 116 | if ([name isEqualToString:@"enclosure"]) |
|---|
| 117 | { |
|---|
| 118 | // enclosure is flattened as a separate dictionary for some reason |
|---|
| 119 | NSEnumerator *attributeEnum = [[(NSXMLElement *)node attributes] objectEnumerator]; |
|---|
| 120 | NSXMLNode *attribute; |
|---|
| 121 | NSMutableDictionary *encDict = [NSMutableDictionary dictionary]; |
|---|
| 122 | |
|---|
| 123 | while ((attribute = [attributeEnum nextObject])) |
|---|
| 124 | [encDict setObject:[attribute stringValue] forKey:[attribute name]]; |
|---|
| 125 | [dict setObject:encDict forKey:@"enclosure"]; |
|---|
| 126 | |
|---|
| 127 | } |
|---|
| 128 | else if ([name isEqualToString:@"pubDate"]) |
|---|
| 129 | { |
|---|
| 130 | // pubDate is expected to be an NSDate by SUAppcastItem, but the RSS class was returning an NSString |
|---|
| 131 | NSDate *date = [NSDate dateWithNaturalLanguageString:[node stringValue]]; |
|---|
| 132 | if (date) |
|---|
| 133 | [dict setObject:date forKey:name]; |
|---|
| 134 | } |
|---|
| 135 | else if (name != nil) |
|---|
| 136 | { |
|---|
| 137 | // add all other values as strings |
|---|
| 138 | [dict setObject:[[node stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] forKey:name]; |
|---|
| 139 | } |
|---|
| 140 | } |
|---|
| 141 | |
|---|
| 142 | SUAppcastItem *anItem = [[SUAppcastItem alloc] initWithDictionary:dict]; |
|---|
| 143 | if (anItem) |
|---|
| 144 | { |
|---|
| 145 | [appcastItems addObject:anItem]; |
|---|
| 146 | [anItem release]; |
|---|
| 147 | } |
|---|
| 148 | else |
|---|
| 149 | { |
|---|
| 150 | NSLog(@"Sparkle Updater: Failed to parse appcast item with appcast dictionary %@!", dict); |
|---|
| 151 | } |
|---|
| 152 | [nodesDict removeAllObjects]; |
|---|
| 153 | [dict removeAllObjects]; |
|---|
| 154 | } |
|---|
| 155 | } |
|---|
| 156 | |
|---|
| 157 | [document release]; |
|---|
| 158 | |
|---|
| 159 | if ([appcastItems count]) |
|---|
| 160 | { |
|---|
| 161 | NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO] autorelease]; |
|---|
| 162 | [appcastItems sortUsingDescriptors:[NSArray arrayWithObject:sort]]; |
|---|
| 163 | items = [appcastItems copy]; |
|---|
| 164 | } |
|---|
| 165 | |
|---|
| 166 | if (failed) |
|---|
| 167 | { |
|---|
| 168 | [self reportError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while parsing the update feed.", nil), NSLocalizedDescriptionKey, nil]]]; |
|---|
| 169 | } |
|---|
| 170 | else if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:)]) |
|---|
| 171 | { |
|---|
| 172 | [delegate appcastDidFinishLoading:self]; |
|---|
| 173 | } |
|---|
| 174 | } |
|---|
| 175 | |
|---|
| 176 | - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error |
|---|
| 177 | { |
|---|
| 178 | CFRelease(download); |
|---|
| 179 | |
|---|
| 180 | [[NSFileManager defaultManager] removeFileAtPath:downloadFilename handler:nil]; |
|---|
| 181 | [downloadFilename release]; |
|---|
| 182 | downloadFilename = nil; |
|---|
| 183 | |
|---|
| 184 | [self reportError:error]; |
|---|
| 185 | } |
|---|
| 186 | |
|---|
| 187 | - (NSURLRequest *)download:(NSURLDownload *)download willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse |
|---|
| 188 | { |
|---|
| 189 | return request; |
|---|
| 190 | } |
|---|
| 191 | |
|---|
| 192 | - (void)reportError:(NSError *)error |
|---|
| 193 | { |
|---|
| 194 | if ([delegate respondsToSelector:@selector(appcast:failedToLoadWithError:)]) |
|---|
| 195 | { |
|---|
| 196 | [delegate appcast:self failedToLoadWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]]; |
|---|
| 197 | } |
|---|
| 198 | } |
|---|
| 199 | |
|---|
| 200 | - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes |
|---|
| 201 | { |
|---|
| 202 | // We use this method to pick out the localized version of a node when one's available. |
|---|
| 203 | if ([nodes count] == 1) |
|---|
| 204 | return [nodes objectAtIndex:0]; |
|---|
| 205 | else if ([nodes count] == 0) |
|---|
| 206 | return nil; |
|---|
| 207 | |
|---|
| 208 | NSEnumerator *nodeEnum = [nodes objectEnumerator]; |
|---|
| 209 | NSXMLElement *node; |
|---|
| 210 | NSMutableArray *languages = [NSMutableArray array]; |
|---|
| 211 | NSString *lang; |
|---|
| 212 | NSInteger i; |
|---|
| 213 | while ((node = [nodeEnum nextObject])) |
|---|
| 214 | { |
|---|
| 215 | lang = [[node attributeForName:@"xml:lang"] stringValue]; |
|---|
| 216 | [languages addObject:(lang ?: @"")]; |
|---|
| 217 | } |
|---|
| 218 | lang = [[NSBundle preferredLocalizationsFromArray:languages] objectAtIndex:0]; |
|---|
| 219 | i = [languages indexOfObject:([languages containsObject:lang] ? lang : @"")]; |
|---|
| 220 | if (i == NSNotFound) |
|---|
| 221 | i = 0; |
|---|
| 222 | return [nodes objectAtIndex:i]; |
|---|
| 223 | } |
|---|
| 224 | |
|---|
| 225 | - (void)setUserAgentString:(NSString *)uas |
|---|
| 226 | { |
|---|
| 227 | if (uas != userAgentString) |
|---|
| 228 | { |
|---|
| 229 | [userAgentString release]; |
|---|
| 230 | userAgentString = [uas copy]; |
|---|
| 231 | } |
|---|
| 232 | } |
|---|
| 233 | |
|---|
| 234 | - (void)setDelegate:del |
|---|
| 235 | { |
|---|
| 236 | delegate = del; |
|---|
| 237 | } |
|---|
| 238 | |
|---|
| 239 | @end |
|---|