1/* UpdateController.m
2 * Checking for Updates...
3 *
4 * Copyright 2010-2012 by vhf interservice GmbH
5 * Author:   Georg Fleischmann
6 *
7 * created:  2010-05-27
8 * modified: 2012-02-07 (-connectionDidFinishLoading: use -writeToFile:...encoding:error:)
9 *           2011-03-30 (-checkForUpdates: test for Prefs_DisableAutoUpdate)
10 *
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of the vhf Public License as
13 * published by vhf interservice GmbH. Among other things, the
14 * License requires that the copyright notices and this notice
15 * be preserved on all copies.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
20 * See the vhf Public License for more details.
21 *
22 * You should have received a copy of the vhf Public License along
23 * with this program; see the file LICENSE. If not, write to vhf.
24 *
25 * vhf interservice GmbH, Im Marxle 3, 72119 Altingen, Germany
26 * eMail: service@vhf.de
27 * http://www.vhf-interservice.com
28 */
29
30#include <AppKit/AppKit.h>
31#include "UpdateController.h"
32#include "App.h"
33#include "locations.h"
34#include "messages.h"
35#include "CenonModuleMethods.h"
36#include "PreferencesMacros.h"  // Prefs_DisableAutoUpdate
37#include <VHFShared/VHFStringAdditions.h>       // -writeToFile:...
38
39static UpdateController *sharedInstance = nil;  // default object
40static NSString         *checkMarkStr = nil;    // the checkmark
41
42@interface UpdateController(PrivateMethods)
43- (void)loadPanel:sender;
44@end
45@interface UpdateTableData:NSObject
46{
47    NSMutableDictionary *dataDict;
48    int                 rowCount;
49}
50/* class methods */
51+ (UpdateTableData*)tableData;
52- (int)numberOfRowsInTableView:(NSTableView*)table;
53- (id)tableView:(NSTableView*)table objectValueForTableColumn:(NSTableColumn*)column
54            row:(int)rowIndex;
55- (void)setDataDict:(NSDictionary*)newDataDict;
56- (NSDictionary*)dataDict;
57@end
58
59@interface NSFileManager(UpdateControllerMethods)
60- (BOOL)fileExistsAtWildcardPath:(NSString*)path;
61@end
62@implementation NSFileManager(UpdateControllerMethods)
63- (BOOL)fileExistsAtWildcardPath:(NSString*)path
64{   NSString    *wildcard = [path lastPathComponent];
65    NSArray     *array;
66    int         i, cnt, j;
67    NSArray     *components = [wildcard componentsSeparatedByString:@"*"];
68
69    if ( [components count] == 1 )
70        return [self fileExistsAtPath:path];
71    path = [path stringByDeletingLastPathComponent];
72    if ( !(array = [self directoryContentsAtPath:path]) || ![components count] )
73        return NO;
74    for ( i=0, cnt = [array count]; i<cnt; i++ )
75    {   NSString    *file = [array objectAtIndex:i];
76        NSRange     searchRange = NSMakeRange(0, [file length]);
77        BOOL        hit = YES;
78
79        for ( j=0; j<[components count]; j++ )
80        {   NSString    *compo = [components objectAtIndex:j];
81            NSRange     range;
82
83            if ( ![compo length] )
84                continue;
85            if ( (range = [file rangeOfString:compo options:0 range:searchRange]).length )
86            {   searchRange.location = range.location + range.length;
87                searchRange.length   = [file length] - searchRange.location;
88            }
89            else
90            {   hit = NO; break; }
91        }
92        if ( hit )
93            return YES;
94    }
95    return NO;
96}
97@end
98
99/*#ifndef MAC_OS_X_VERSION_10_6
100#define MAC_OS_X_VERSION_10_6 1060
101#endif*/
102//#if defined(__APPLE__) && MACOSX_DEPLOYMENT_TARGET <= MAC_OS_X_VERSION_10_6
103/* available OSX >= 10.6, not on GNUstep yet (2010-09-20) */
104@interface NSWorkspace(UpdateControllerMethods)
105- (void)activateFileViewerSelectingURLs:(NSArray*)fileURLs;
106@end
107
108
109@implementation UpdateController
110
111+ (UpdateController*)sharedInstance
112{
113    if (!sharedInstance)
114        sharedInstance = [self new];
115    return sharedInstance;
116}
117- (id)init
118{   char    checkMarkChars[4] = {0xE2, 0x9C, 0x93, 0};  // unicode
119
120    [super init];
121    checkMarkStr = [[NSString stringWithUTF8String:checkMarkChars] retain];
122    updateDict   = nil;
123    [self loadPanel:self];  // this loads the interface and has to come first
124    return self;
125}
126
127/* show panel
128 * load interface file and display panel
129 */
130- (void)loadPanel:sender
131{
132    if ( !panel )
133    {   NSBundle	*bundle = [NSBundle mainBundle];
134
135        /* load panel, this establishes connections to interface outputs */
136        if ( ![bundle loadNibFile:@"UpdatePanel"
137                externalNameTable:[NSDictionary dictionaryWithObject:self forKey:@"NSOwner"]
138                         withZone:[self zone]] )
139            NSLog(@"Cannot load Update Panel interface file");
140        [panel setDelegate:self];
141        [panel setFrameUsingName:@"UpdatePanel"];
142        [panel setFrameAutosaveName:@"UpdatePanel"];
143
144        [tableView setDelegate:self];
145        [tableView setTarget:self];
146        [tableView setAction:@selector(tableClick:)];
147        //[tableView setDoubleAction:@selector(doubleClick:)];
148    }
149}
150
151/* return Dictionary with installed modules and it's versions
152 */
153- (NSDictionary*)installedModules
154{   NSMutableDictionary *installedDict = [NSMutableDictionary dictionary];
155    NSArray             *modules = [(App*)NSApp modules];
156    int                 i;
157
158    for ( i=0; i<[modules count]; i++ ) // modules
159    {   NSBundle        *bundle = [modules objectAtIndex:i]; // our loaded modules
160        NSDictionary    *infoDict = [bundle infoDictionary];
161        NSString        *version = [infoDict objectForKey:@"CFBundleVersion"];
162        NSString        *name    = [infoDict objectForKey:@"CFBundleExecutable"];   // CAM, Astro, AstroFractal
163        NSString        *date    = nil, *serial = nil, *netId = nil;
164
165        if ( [[[bundle principalClass] instance] respondsToSelector:@selector(compileDate)] )
166            date = [[[bundle principalClass] instance] compileDate];
167        if ( [[[bundle principalClass] instance] respondsToSelector:@selector(serialNo)] )
168            serial = [[[bundle principalClass] instance] serialNo];
169        if ( [[[bundle principalClass] instance] respondsToSelector:@selector(netId)] )
170            netId = [[[bundle principalClass] instance] netId];
171        [installedDict setObject:[NSArray arrayWithObjects:version, (date) ? date : @"",
172                                  (serial) ? serial : @"", (netId) ? netId : @"", nil]
173                          forKey:name];
174        if ( !isAutoCheck )
175            printf("Installed Modules: %s = %s (%s) %s %s\n", [name UTF8String], [version UTF8String],
176               (date) ? [date UTF8String] : "", (serial) ? [serial UTF8String] : "",
177               (netId) ? [netId UTF8String] : "");
178    }
179    return installedDict;
180}
181
182/* tries to download update.plist from vhf server
183 */
184- (void)checkForUpdates:sender
185{   NSDictionary    *installedModules;
186    NSArray         *keys;
187    NSURLRequest    *connectionRequest;
188    NSMutableString *urlStr = [NSMutableString string];
189    NSString        *appVersion = [(App*)NSApp version];        // 3.9.2
190    NSString        *appDate    = [(App*)NSApp compileDate];    // 2010-06-28
191    NSUserDefaults  *userDefaults = [NSUserDefaults standardUserDefaults];
192    NSString        *skipVersion     = [userDefaults objectForKey:@"skipUpdateVersion"];
193    NSDate          *lastUpdateCheck = [userDefaults objectForKey:@"lastUpdateCheck"];
194    int             i;
195
196    isAutoCheck = ([sender isKindOfClass:[NSMenuItem class]]) ? NO : YES;
197    if ( isAutoCheck && Prefs_DisableAutoUpdate )
198        return; // automatic check is disabled in preferences
199    if ( isAutoCheck && lastUpdateCheck &&
200         [[NSDate date] timeIntervalSinceDate:lastUpdateCheck] < 3600*24*7 )
201        return; // not yet one week -> no check
202
203    installedModules = [self installedModules];
204    [urlStr appendString:@"http://www.cenon.info/cgi-bin/updateCenon"];
205    [urlStr appendFormat:@"?n=%@&v=%@&d=%@", APPNAME, appVersion, appDate];
206    if ( [skipVersion length] ) // version the user wants to skip
207        [urlStr appendFormat:@"&sk=%@", skipVersion];
208    //[urlStr appenFormat:@"&p="];    // TODO: pass plattform to update script
209    for (keys = [installedModules allKeys], i=0; i < [keys count]; i++)
210    {   int         n = i+1;
211        NSString    *name = [keys objectAtIndex:i];
212        NSArray     *array = [installedModules objectForKey:name];
213        NSString    *version = [array objectAtIndex:0];
214        NSString    *date    = [array objectAtIndex:1];
215        NSString    *serial  = [array objectAtIndex:2];
216        NSString    *netId   = [array objectAtIndex:3];
217
218        [urlStr appendFormat:@"&n%d=%@", n, name];
219        if ( [version length] ) [urlStr appendFormat:@"&v%d=%@", n, version];
220        if ( [date    length] ) [urlStr appendFormat:@"&d%d=%@", n, date];
221        if ( [serial  length] ) [urlStr appendFormat:@"&s%d=%@", n, serial];
222        if ( [netId   length] ) [urlStr appendFormat:@"&o%d=%@", n, netId];     // origin
223    }
224    connectionRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]
225                                         cachePolicy:NSURLRequestReloadIgnoringCacheData
226                                     timeoutInterval:60.0];
227    urlConnection = [[NSURLConnection alloc] initWithRequest:connectionRequest delegate:self];
228    if ( urlConnection )
229    {   connectionData = [[NSMutableData alloc] init];
230        [progressTitleText setStringValue:@""];
231        [progressNameText setStringValue:@""];
232        [progressSizeText setStringValue:@""];
233        [progressIndicator setIndeterminate:YES];
234        [progressIndicator startAnimation:nil];
235        [progressPanel makeKeyAndOrderFront:self];
236        [userDefaults setObject:[NSDate date] forKey:@"lastUpdateCheck"];   // date of this check
237    }
238#if 0   // this downloads a plist file directly (what, if it ends up in a proxy ?)
239    NSURL           *url = [NSURL URLWithString:@"http://www.cenon.info/update/update.plist"];
240    NSString        *path = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update.plist", nil);
241    NSURLRequest    *request;
242    NSURLDownload   *download;
243
244    if ( checking || pkgDownload )
245        return;
246    checking = YES;
247    request = [NSURLRequest requestWithURL:url];
248    download = [[NSURLDownload alloc] initWithRequest:request delegate:self];
249    if ( [sender isKindOfClass:[NSMenuItem class]] )    // we remove the skipped version
250        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"skipUpdateVersion"];
251    [download setDeletesFileUponFailure:YES];
252    [download setDestination:path allowOverwrite:YES];
253#endif
254}
255
256/* check for available updates, if available display panel
257 * created:  2010-05-27
258 * modified: 2011-02-22 (don't offer update of uninstalled versions)
259 * FIXME: this will not work with GNUstep, we need to access the infoDict correctly or add Apple info
260 */
261- (void)checkForUpdateAndDisplayPanel
262{   static NSString     *infoString = nil;  // used to memorize infoLabel
263    NSMutableDictionary *dataDict = [NSMutableDictionary dictionary];
264    NSArray             *modules = [(App*)NSApp modules], *instKeys, *updateKeys;
265    NSDictionary        *infoDict = [[NSBundle mainBundle] infoDictionary];
266    NSString            *appVersion = [(App*)NSApp version];                            // "3.9.2"
267    NSString            *appName    = [infoDict objectForKey:@"CFBundleExecutable"];    // Cenon, APPNAME
268    NSString            *appDate    = [(App*)NSApp compileDate];
269    NSMutableDictionary *installedDict = [NSMutableDictionary dictionary];
270    NSMutableArray      *checkOrder = [NSMutableArray array];
271    int                 i, j;
272    NSString            *updateFile;
273    NSString            *skipVersion = [[NSUserDefaults standardUserDefaults] objectForKey:@"skipUpdateVersion"];
274    NSString            *newVersion;
275    BOOL                updateAvailable = NO;
276    NSFileManager       *fileManager = [NSFileManager defaultManager];
277
278    [installButton setEnabled:NO];
279
280    /* Update.plist */
281    updateFile = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update.plist", nil);
282    if ( updateDict )
283        [updateDict release];
284    updateDict = [[NSDictionary dictionaryWithContentsOfFile:updateFile] retain];
285    updateKeys = [updateDict allKeys];
286    newVersion = [[updateDict objectForKey:appName] objectForKey:@"v"];
287    if ( isAutoCheck && newVersion && skipVersion && [newVersion isEqual:skipVersion] )
288        return;
289
290    /* Installed versions of App, Modules, Fonts, etc. */
291    if ( !appName )
292        appName = APPNAME;
293    //infoVersion = [[(App*)NSApp infoVersionNo] stringValue];    // ex: 3.9.1 pre 1 (2010-02-13)
294    [installedDict setObject:[NSArray arrayWithObjects:appVersion, appDate, nil] forKey:appName];
295    [checkOrder addObject:appName];
296    if ( !isAutoCheck )
297        printf("Installed App:     %s = %s (%s)\n", [appName UTF8String], [appVersion UTF8String], [appDate UTF8String]);
298    // FIXME: this is basically duplicating -installedModules and should be united
299    for ( i=0; i<[modules count]; i++ ) // modules
300    {   NSBundle        *bundle = [modules objectAtIndex:i]; // our loaded modules
301        NSDictionary    *infoDict = [bundle infoDictionary];
302        NSString        *version = [infoDict objectForKey:@"CFBundleVersion"];
303        NSString        *name    = [infoDict objectForKey:@"CFBundleExecutable"];   // CAM, Astro, AstroFractal
304        NSString        *date    = nil;
305
306        if ( [[[bundle principalClass] instance] respondsToSelector:@selector(compileDate)] )
307            date = [[[bundle principalClass] instance] compileDate];
308        [installedDict setObject:[NSArray arrayWithObjects:version, date, nil] forKey:name];
309        [checkOrder addObject:name];
310        //if ( !isAutoCheck )
311        //    printf("Installed Modules: %s = %s (%s)\n", [name UTF8String], [version UTF8String], (date) ? [date UTF8String] : "");
312    }
313    if ( [updateDict objectForKey:@"order"] )
314        checkOrder = [updateDict objectForKey:@"order"];
315    /* Check for install paths of additional items given in update.plist and add their versions etc. */
316    for ( i=0; i<[updateKeys count]; i++ )
317    {   NSString        *key = [updateKeys objectAtIndex:i], *path, *pathU = nil;
318        NSDictionary    *uDict = [updateDict objectForKey:key];
319
320        if ( ![uDict isKindOfClass:[NSDictionary class]] || !(path = [uDict objectForKey:@"c"]) )
321            continue;
322        if ( ! [path isAbsolutePath] )
323        {   pathU = vhfPathWithPathComponents(NSHomeDirectory(),         path, nil);
324            path  = vhfPathWithPathComponents(NSOpenStepRootDirectory(), path, nil);
325        }
326        if (           [fileManager fileExistsAtWildcardPath:path] ||
327             (pathU && [fileManager fileExistsAtWildcardPath:pathU]) )
328        {   NSString    *version = [uDict objectForKey:@"v"];   // TODO: version of additional installed stuff
329            NSString    *date    = nil;                         // TODO: date of stuff
330            [installedDict setObject:[NSArray arrayWithObjects:version, date, nil] forKey:key];
331        }
332    }
333    instKeys = [installedDict allKeys];
334
335    [dataDict setObject:[NSMutableArray array] forKey:@"install"];
336    [dataDict setObject:[NSMutableArray array] forKey:@"name"];
337    [dataDict setObject:[NSMutableArray array] forKey:@"version"];
338    [dataDict setObject:[NSMutableArray array] forKey:@"size"];
339    [dataDict setObject:[NSMutableArray array] forKey:@"price"];
340    [dataDict setObject:[NSMutableArray array] forKey:@"updateKey"];    // our key to find way back
341    [dataDict setObject:[NSMutableArray array] forKey:@"url"];          // download link
342
343    /* Compare available updates with installed versions */
344    for ( i=0; i<[checkOrder count]; i++ )  // CAM, AstroFractal, Astro, Cenon
345    {   NSString        *key = [checkOrder objectAtIndex:i];
346        NSDictionary    *uDict  = [updateDict objectForKey:key];    // v, s, l, i, n, r, d
347        NSArray         *iArray = [installedDict objectForKey:key];
348        NSString        *version  = [uDict objectForKey:@"v"];      // package version
349        NSString        *mVersion = [uDict objectForKey:@"mv"];     // module version
350        NSString        *name     = [uDict objectForKey:@"n"];
351        NSString        *size     = [uDict objectForKey:@"s"], *price, *url;
352        BOOL            installModule = NO; // install complete package or module ?
353
354        if ( ! version || ! name )
355            continue;
356        /* is item eclipsed in rel "r" (ex: Cenon+Module.pkg eclipses Cenon.pkg alone) ? */
357        {   BOOL    eclipsed = NO;
358
359            for ( j=0; j<[instKeys count]; j++ )    // all installed stuff can eclipse
360            {   NSString        *instKey = [instKeys objectAtIndex:j];
361                NSDictionary    *uDictI = [updateDict objectForKey:instKey];
362                NSArray         *relArray;
363                int             k;
364
365                if ( ![uDictI isKindOfClass:[NSDictionary class]] )
366                    continue;
367                relArray = [uDictI objectForKey:@"r"];
368                for (k=0; k<[relArray count]; k++ ) // it's relationships: "-" will eclipse
369                {   NSString    *relKey = [relArray objectAtIndex:k];
370
371                    if ( ! [relKey hasPrefix:@"-"] )
372                        continue;
373                    relKey = [relKey substringFromIndex:1];
374                    if ( [key isEqual:relKey]  )    // key eclipsed from relations of installed item
375                    {   eclipsed = YES; break; }    // eclipsed
376                }
377                if (eclipsed)
378                    break;
379            }
380            if ( eclipsed )
381                continue;
382        }
383
384        /* compare installed versions with available updates */
385        if ( iArray )   // installed item
386        {   NSString    *v = (mVersion) ? mVersion : version;   // we compile module version
387
388            if ( [v appearanceCountOfCharacter:'.'] <= 1 )  // workaround for single dot version numbers "1.11" -> "1.1.1"
389                v = @"0.0.0"; // this is old anyway
390            if ( [[iArray objectAtIndex:0] compare:(id)v options:NSNumericSearch] == NSOrderedAscending )   // installed < version
391                updateAvailable = YES;
392            // TODO: if version is the same, compare date
393            else    // installed >= version
394                continue;
395        }
396        else    // 2011-02-22: not installed -> we don't offer item for installation
397            continue;
398
399        /* check, wether to install module instead of Package (ex: CenonCAM + Astro-Module) */
400        url = ([uDict objectForKey:@"d"]) ? [uDict objectForKey:@"d"] : @"";
401        if ( mVersion && [uDict objectForKey:@"ms"] && [uDict objectForKey:@"md"] )
402        {
403            for ( j=0; j<[instKeys count]; j++ )    // all installed stuff
404            {   NSString        *instKey = [instKeys objectAtIndex:j];
405                NSDictionary    *uDictI = [updateDict objectForKey:instKey];
406
407                if ( ![uDictI isKindOfClass:[NSDictionary class]] )
408                    continue;
409                /* if something is installed, then we installed a Cenon-Package already */
410                if ( [(NSArray*)[dataDict objectForKey:@"updateKey"] count] )
411                {   installModule = YES;
412                    url = [uDict objectForKey:@"md"];
413                    version = mVersion;
414                    size    = [uDict objectForKey:@"ms"];
415                    break;
416                }
417            }
418        }
419
420        /* Add item */
421        if ( [url length] )
422            [installButton setEnabled:YES]; // enable install button, if something is there to install
423        [[dataDict objectForKey:@"install"]   addObject:([url length]) ? @"1" : @"0"];
424        [[dataDict objectForKey:@"name"]      addObject:name];  // name
425        [[dataDict objectForKey:@"size"]      addObject:(size) ? size : @""];
426        [[dataDict objectForKey:@"updateKey"] addObject:key];   // key for reference
427        [[dataDict objectForKey:@"url"]       addObject:url];
428        /* License or Price */
429        if ( [[uDict objectForKey:@"l"] hasPrefix:@"f"] )       // price or free
430            price = NSLocalizedString(@"free", NULL);
431        else
432        {   NSDictionary    *priceDict = [uDict objectForKey:@"p"];
433
434            price = nil;
435            if ( priceDict )
436            {   //NSString    *v = ([uDict objectForKey:@"mv"]) ? [uDict objectForKey:@"mv"] : version;
437                NSString    *v = ([iArray count]) ? [iArray objectAtIndex:0] : appVersion;  // version of installed module
438                NSRange     range = [v rangeOfString:@"." options:NSBackwardsSearch];
439
440                if ( range.length )
441                {   NSString    *vMajor = [v substringToIndex:range.location]; // "3.9", "1.1"
442
443                    if ( iArray )   // installed
444                    {   price = [priceDict objectForKey:vMajor];
445                        if ( [price isEqual:@"0.0"] )
446                            price = NSLocalizedString(@"free", NULL);
447                    }
448                    else            // not installed (2011-02-22)
449                        price = [priceDict objectForKey:@"0"];
450                }
451            }
452            if ( !price )
453                price = @"";
454        }
455        [[dataDict objectForKey:@"price"]     addObject:price];
456        if ( ! installModule && [uDict objectForKey:@"mv"] )    // version
457            version = [version stringByAppendingFormat:@" (%@)", [uDict objectForKey:@"mv"]];
458        [[dataDict objectForKey:@"version"]   addObject:version];
459    }
460
461
462    /* Add additional items for installed items (Fonts, Ephemeris, ...)
463     */
464    for ( i=0; i<[(NSArray*)[dataDict objectForKey:@"updateKey"] count]; i++ )
465    {   NSString        *key = [[dataDict objectForKey:@"updateKey"] objectAtIndex:i];
466        NSDictionary    *uDict  = [updateDict objectForKey:key];    // v, s, l, i, n, r
467        NSArray         *relArray = [uDict objectForKey:@"r"];
468
469        for ( j=0; j<[relArray count]; j++ )
470        {   NSString        *key = [relArray objectAtIndex:j];
471            NSDictionary    *uDictR = [updateDict objectForKey:key];
472            NSString        *name    = [uDictR objectForKey:@"n"];
473            NSString        *size    = [uDictR objectForKey:@"s"];
474            NSString        *version = [uDictR objectForKey:@"v"], *price;
475            NSString        *url = ([uDictR objectForKey:@"d"]) ? [uDictR objectForKey:@"d"] : @"";
476            NSArray         *iArray;
477
478            if ( [[dataDict objectForKey:@"updateKey"] containsObject:key]
479                 || !name || !version )
480                continue;
481            /* check available version against installed stuff */
482            iArray = [installedDict objectForKey:key];
483            if ( [instKeys containsObject:key] &&
484                 ([iArray count] && [[iArray objectAtIndex:0] compare:(id)version options:NSNumericSearch] != NSOrderedAscending) )
485                continue;   // installed and up to date
486            /* compare installed versions with available updates */
487            if ( iArray )   // installed item
488            {
489                if ( [[iArray objectAtIndex:0] compare:(id)version options:NSNumericSearch] == NSOrderedAscending ) // version > installed
490                    updateAvailable = YES;
491            }
492            [[dataDict objectForKey:@"install"]   addObject:@"0"];
493            [[dataDict objectForKey:@"name"]      addObject:name];
494            [[dataDict objectForKey:@"version"]   addObject:version];
495            [[dataDict objectForKey:@"size"]      addObject:(size) ? size : @""];
496            [[dataDict objectForKey:@"updateKey"] addObject:key];
497            [[dataDict objectForKey:@"url"]       addObject:url];
498            if ( [[uDictR objectForKey:@"l"] hasPrefix:@"f"] )
499                price = NSLocalizedString(@"free", NULL);
500            else
501            {   //NSDictionary    *priceDict = [uDictR objectForKey:@"p"];
502
503                price = nil;
504                /*if ( priceDict )  // TODO: prices for additional stuff
505                {   NSString    *v = ([uDict objectForKey:@"mv"]) ? [uDict objectForKey:@"mv"] : version;
506                    NSRange     range = [v rangeOfString:@"." options:NSBackwardsSearch];
507
508                    if ( range.length )
509                    {   NSString    *vMajor = [v substringToIndex:range.location]; // "3.9", "1.1"
510                        price = [priceDict objectForKey:vMajor];
511                    }
512                }*/
513                if ( !price )
514                    price = @"";
515            }
516            [[dataDict objectForKey:@"price"]     addObject:price];
517        }
518    }
519
520    if ( !tableData )
521        tableData = [UpdateTableData new];
522    [tableData setDataDict:dataDict];
523
524    //[self loadPanel:self];  // this loads the interface and has to come first
525
526    /* update title and info labels */
527    if ( updateAvailable )
528    {
529        [titleLabel setStringValue:NSLocalizedString(@"A new version of Cenon is available", NULL)];
530        if ( !infoString )
531            infoString = [[infoLabel stringValue] retain];
532        if ( newVersion )
533        {   NSString    *str = [infoString stringByReplacing:@"V_NEW" by:newVersion all:NO];
534            str = [str stringByReplacing:@"V_INST" by:appVersion all:NO];
535            [infoLabel setStringValue:str];
536        }
537        else
538            [infoLabel setStringValue:@""];
539    }
540    else
541    {   [titleLabel setStringValue:NSLocalizedString(@"Cenon is up to date", NULL)];
542        if ( !infoString )
543            infoString = [[infoLabel stringValue] retain];
544        [infoLabel  setStringValue:@""];
545    }
546
547    [tableView setDataSource:tableData];
548    [tableView reloadData];
549    if ( updateAvailable )
550        [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
551
552    if ( [updateFile hasSuffix:@"update.plist"] )
553        [[NSFileManager defaultManager] removeFileAtPath:updateFile handler:nil];   // <= 10.5
554        //[[NSFileManager defaultManager] removeItemAtPath:updateFile error:NULL];  // >= 10.6
555
556    if ( updateAvailable || !isAutoCheck )  // only display panel if update is available or manual
557        [panel makeKeyAndOrderFront:self];
558}
559
560/* Action methods
561 * We collect the files that we have to download and start downloading the first one.
562 * When the 1st file finished, -downloadDidFinish: continues with the other files.
563 */
564- (void)install:sender
565{   int             i;
566    NSDictionary    *dataDict = [tableData dataDict];
567    NSArray         *keys = [dataDict objectForKey:@"updateKey"];
568
569    /* determine number of files to download */
570    [downloadFiles release];
571    downloadFiles = [[NSMutableArray array] retain];
572    for ( i=0, fileCnt=0; i < [keys count]; i++ )
573    {
574        if ( [[[dataDict objectForKey:@"install"] objectAtIndex:i] intValue] )
575        {   NSString    *urlStr = [[dataDict objectForKey:@"url"] objectAtIndex:i];
576
577            /* start download of file */
578            if ( [urlStr length] )
579            {   NSURL       *url = [NSURL URLWithString:urlStr];
580                NSString    *fileName = [urlStr lastPathComponent], *path = nil;
581                NSArray     *array = nil;
582
583#ifdef __APPLE__
584                if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_5)
585                    array = NSSearchPathForDirectoriesInDomains(15/*NSDownloadsDirectory*/, NSUserDomainMask, YES);
586                if ( ![array count] && NSAppKitVersionNumber >= NSAppKitVersionNumber10_4)
587                    array = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
588#endif
589                if ( array && [array count] )   // 1. $HOME/Downloads/PACKAGE
590                    path = [[array objectAtIndex:0] stringByAppendingPathComponent:fileName];
591                if ( !path )                    // 2. /var/folders/8L/.../-Tmp-/PACKAGE
592                    path = vhfPathWithPathComponents(NSTemporaryDirectory(), fileName, nil);   // this is a nonsense folder: "/var/folders/8L/arg/-Tmp-"
593                if ( !path )                    // 3. /tmp/PACKAGE
594                    path = vhfPathWithPathComponents(NSOpenStepRootDirectory(), @"tmp", fileName, nil);
595                fileCnt ++;
596                [downloadFiles addObject:[NSArray arrayWithObjects:url, path, nil]];    // url, path
597            }
598            else
599                NSLog(@"Update-Controller: Nothing to install for '%@'",
600                      [[dataDict objectForKey:@"name"] objectAtIndex:i]);
601        }
602    }
603
604    if ( fileCnt )
605    {   int             fileIx = [downloadFiles count] - fileCnt;
606        NSArray         *array = [downloadFiles objectAtIndex:fileIx];
607        NSURL           *url  = [array objectAtIndex:0];
608        NSString        *path = [array objectAtIndex:1];
609        NSURLRequest    *request = [NSURLRequest requestWithURL:url];
610        NSString        *titleStr;
611
612        pkgDownload = [[NSURLDownload alloc] initWithRequest:request delegate:self];
613        [pkgDownload setDeletesFileUponFailure:YES];
614        pkgPath = path;
615        [pkgDownload setDestination:pkgPath allowOverwrite:YES];
616        titleStr = [NSString stringWithFormat:NSLocalizedString(@"Downloading %d %@", NULL),
617                    fileCnt, (fileCnt > 1) ? NSLocalizedString(@"Items", NULL) : NSLocalizedString(@"Item", NULL)];
618        [progressTitleText setStringValue:titleStr];
619        [progressNameText setStringValue:[path lastPathComponent]];
620        [progressSizeText setStringValue:@""];
621        [progressIndicator setIndeterminate:YES];
622        [progressIndicator startAnimation:nil];
623        [progressPanel makeKeyAndOrderFront:self];
624        printf("UpdateController: Download %s\n", [[url absoluteString] UTF8String]);
625    }
626    [panel orderOut:self];
627}
628- (void)skip:sender
629{   NSUserDefaults  *defaults = [NSUserDefaults standardUserDefaults];
630    NSDictionary    *infoDict = [[NSBundle mainBundle] infoDictionary];
631    NSString        *appName = [infoDict objectForKey:@"CFBundleExecutable"];    // Cenon, APPNAME
632    NSString        *newVersion;
633
634    if ( !appName )
635        appName = APPNAME;
636    newVersion = [[updateDict objectForKey:appName] objectForKey:@"v"];
637    if ( newVersion )
638        [defaults setObject:newVersion forKey:@"skipUpdateVersion"];
639    [panel orderOut:self];
640}
641/* Close Update-Panel
642 */
643- (void)cancel:sender
644{
645    [panel orderOut:self];
646}
647
648
649/* Cancel the download from within Progress Panel
650 */
651- (void)cancelDownload:sender
652{
653    if (urlConnection)
654    {   [urlConnection cancel];
655        [urlConnection release]; urlConnection = nil;
656    }
657    if (pkgDownload)
658    {
659        [pkgDownload cancel];
660        [pkgDownload release]; pkgDownload = nil;
661    }
662    [progressPanel orderOut:self];
663}
664
665/* click into the table - checkmark
666 * created:  2010-05-31
667 * modified: 2010-06-01
668 */
669- (void)tableClick:sender
670{   int rowIx = [sender clickedRow];
671    int colIx = [sender clickedColumn];
672
673    if ( rowIx < 0 )
674        return;
675    /* Checkmark row */
676    if ( colIx == 0 )   // checkmark row
677    {   NSString        *colId = [[[sender tableColumns] objectAtIndex:colIx] identifier];
678        NSMutableArray  *colArray = [[[sender dataSource] dataDict] objectForKey:colId];
679        NSMutableArray  *keyArray = [[[sender dataSource] dataDict] objectForKey:@"updateKey"];
680        NSString        *boolStr = @"";
681        NSString        *url = [[updateDict objectForKey:[keyArray objectAtIndex:rowIx]] objectForKey:@"d"];
682
683        if ( url && // checkmark only for downloadable items
684             (![(NSString*)[colArray objectAtIndex:rowIx] length] || ![[colArray objectAtIndex:rowIx] intValue]) )
685            boolStr = @"1";
686        [colArray replaceObjectAtIndex:rowIx withObject:boolStr];
687    }
688}
689
690/* NSURLConnection Delegate Methods */
691//#if 0
692- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
693{
694    [connectionData setLength:0];
695    [progressTitleText setStringValue:NSLocalizedString(@"Connected to server", NULL)];
696}
697
698- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
699{
700    [connectionData appendData:data];
701    [progressTitleText setStringValue:NSLocalizedString(@"Receiving data", NULL)];
702}
703
704- (void)connectionDidFinishLoading:(NSURLConnection *)connection
705{   NSString    *string = [[[NSString alloc] initWithData:connectionData
706                                                 encoding:NSUTF8StringEncoding] autorelease];
707
708    checking = NO;
709    if ( connection == urlConnection )
710        urlConnection = nil;
711    [connection release];
712    [connectionData release]; connectionData = nil;
713    [progressPanel orderOut:self];
714    if (string && [string length] > 0)
715    {   NSString    *updateFile;
716
717        updateFile = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update.plist", nil);
718        //[string writeToFile:updateFile atomically:NO];
719        [string writeToFile:updateFile atomically:NO
720                   encoding:NSUTF8StringEncoding error:NULL];   // >= 10.5
721        [self checkForUpdateAndDisplayPanel];
722    }
723    // TODO: show "up to date" info
724}
725
726- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
727{
728    checking = NO;
729    if ( connection == urlConnection )
730        urlConnection = nil;
731    [connection release];
732    [connectionData release]; connectionData = nil;
733    [progressPanel orderOut:self];
734}
735
736- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
737                  willCacheResponse:(NSCachedURLResponse *)cachedResponse
738{
739    return nil;     // Never cache
740}
741//#endif
742
743/* NSURLDownload Delegate Methods */
744/* notification: download did finish
745 */
746- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response
747{
748    sizeTotal = [response expectedContentLength];
749    sizeDownl = 0;
750}
751- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(NSUInteger)length
752{
753    sizeDownl += length;
754    if (sizeTotal > 0)
755    {   float       percentComplete = ((float)sizeDownl / (float)sizeTotal) * 100.0;
756        NSString    *sizeStr;
757        float       div = (sizeTotal < 1024*100) ? 1024.0 : (1024.0*1024.0);    // MB or KB
758
759        [progressIndicator setIndeterminate:NO];
760        [progressIndicator setDoubleValue:percentComplete];
761        sizeStr = [NSString stringWithFormat:((sizeTotal < 1024*100) ? @"%.1f KB %@ %.1f KB" : @"%.1f MB %@ %.1f MB"),
762                   sizeDownl/div, NSLocalizedString(@"of", NULL), sizeTotal/div];
763        [progressSizeText setStringValue:sizeStr];
764    }
765}
766- (void)downloadDidFinish:(NSURLDownload*)download
767{   NSURLRequest    *request;
768    NSURL           *url;
769
770    if ( [download isKindOfClass:[NSURLDownload class]] )
771        request = [download request];
772    else    // this is a hack to avoid to many methods
773    {   request = (NSURLRequest*)download;
774        download = nil;
775    }
776    url = [request URL];
777
778    if ( [[url absoluteString] hasSuffix:@"update.plist"] )
779    {   checking = NO;
780        [self checkForUpdateAndDisplayPanel];
781    }
782    else if ( [[url absoluteString] hasSuffix:@".rtf"] )    // info file
783    {   NSString        *path, *name;
784        NSData          *rtfData;
785
786        name = [[url absoluteString] lastPathComponent];
787        path = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update", name, nil);
788        rtfData = [NSData dataWithContentsOfFile:path];
789        if (rtfData)
790            [textView replaceCharactersInRange:NSMakeRange(0, [[textView string] length])
791                                       withRTF:rtfData];
792    }
793    else    // package
794    {
795        fileCnt --;
796
797        if ( [[pkgPath pathExtension] isEqual:@"tar"] )             // unpack TAR archive
798        {   NSString    *path = [pkgPath stringByDeletingLastPathComponent];
799            NSString    *command = [NSString stringWithFormat:@"/usr/bin/tar -x -C %@ -f %@", path, pkgPath];
800
801            system([command UTF8String]);
802            pkgPath = [pkgPath stringByDeletingPathExtension];
803        }
804        if ( ! [[NSWorkspace sharedWorkspace] openFile:pkgPath] )   // open package in Installer
805            NSLog(@"Couldn't open file %@", pkgPath);
806        pkgDownload = nil;
807
808        if ( !fileCnt )
809        {   NSFileManager   *fileManager = [NSFileManager defaultManager];
810            NSString        *path;
811            int             i;
812
813            // TODO: "sudo installer -pkg %@ -target /"   // man installer
814            // -dumplog ... &2 > LOGFILE
815            // -showChoiceChangesXML, -applyChoiceChangesXML <pathToXMLFile>
816
817            /* cleanup files: .update.plist, .update/rtf-files, packages */
818            path = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update.plist", nil);
819            [fileManager removeFileAtPath:path handler:nil];
820            path = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update", nil);
821            [fileManager removeFileAtPath:path handler:nil];
822
823            for ( i=0, path=nil; i<[downloadFiles count]; i++)
824            {   path = [[downloadFiles objectAtIndex:i] objectAtIndex:1];
825                if ( [[path pathExtension] isEqual:@"tar"] && [fileManager fileExistsAtPath:path] ) // del TAR-file
826                    [fileManager removeFileAtPath:path handler:nil];
827                // TODO: the following must not happen before the files are really installed !
828                /*path = [path stringByDeletingPathExtension];
829                if ( [fileManager fileExistsAtPath:path] )  // delete PKG-file
830                    [fileManager removeFileAtPath:path handler:nil];*/
831            }
832            if ( [[NSWorkspace sharedWorkspace] respondsToSelector:@selector(activateFileViewerSelectingURLs:)] )
833            {   NSMutableArray  *array = [NSMutableArray array];
834
835                //path = [path stringByDeletingPathExtension];
836                for ( i=0; i<[downloadFiles count]; i++)
837                {   path = [[downloadFiles objectAtIndex:i] objectAtIndex:1];
838                    path = [path stringByDeletingPathExtension];    //remove ".tar"
839                    [array addObject:[NSURL URLWithString:path]];
840                }
841                [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:array];
842            }
843            [progressPanel orderOut:self];
844            NSRunAlertPanel(@"Update Downloaded", @"You can now install the open packages.", OK_STRING, nil, nil);
845            [downloadFiles release]; downloadFiles = nil;
846        }
847        else
848        {   int             fileIx = [downloadFiles count] - fileCnt;
849            NSArray         *array = [downloadFiles objectAtIndex:fileIx];
850            NSURL           *url  = [array objectAtIndex:0];
851            NSString        *path = [array objectAtIndex:1];
852            NSURLRequest    *request = [NSURLRequest requestWithURL:url];
853            NSString        *titleStr;
854
855            pkgDownload = [[NSURLDownload alloc] initWithRequest:request delegate:self];
856            [pkgDownload setDeletesFileUponFailure:YES];
857            pkgPath = path;
858            [pkgDownload setDestination:pkgPath allowOverwrite:YES];
859            titleStr = [NSString stringWithFormat:NSLocalizedString(@"Downloading %d %@", NULL),
860                        fileCnt, (fileCnt > 1) ? NSLocalizedString(@"Items", NULL) : NSLocalizedString(@"Item", NULL)];
861            [progressTitleText setStringValue:titleStr];
862            [progressNameText setStringValue:[path lastPathComponent]];
863            [progressSizeText setStringValue:@""];
864            [progressIndicator setIndeterminate:YES];
865            [progressIndicator startAnimation:nil];
866            [progressPanel makeKeyAndOrderFront:self];
867            printf("UpdateController: Download %s\n", [[url absoluteString] UTF8String]);
868        }
869    }
870    if ( download == pkgDownload )
871        pkgDownload = nil;
872    [download release];
873}
874- (void)download:(NSURLDownload*)download didFailWithError:(NSError*)error
875{   NSURLRequest    *request = [download request];
876    NSURL           *url = [request URL];
877    //name = [dataDict objectForKey:@"name"];
878
879    if ( [[url absoluteString] hasSuffix:@"update.plist"] )
880        checking = NO;
881    else if ( [[url absoluteString] hasSuffix:@".rtf"] )    // info file
882        [textView replaceCharactersInRange:NSMakeRange(0, [[textView string] length])
883                                withString:@""];
884    else                                                    // package download
885    {
886        NSLog(@"Update-Controller: Download failed for file '%@'", url);
887        [progressTitleText setStringValue:NSLocalizedString(@"Download failed !", NULL)];
888        [downloadFiles release]; downloadFiles = nil;
889        //[progressPanel orderOut:self];
890    }
891    if ( download == pkgDownload )
892        pkgDownload = nil;
893    [download release];
894}
895
896/* NSTableView Delegate methods and Notifications */
897/* notification: selection changed
898 */
899- (void)tableViewSelectionDidChange:(NSNotification*)notification
900{   //NSTableView *tableView = [notification object];
901    int             rowIx = [tableView selectedRow];
902    NSString        *updateKey; // key in updateDict
903    NSDictionary    *uDict, *names;
904    NSURL           *url;
905    NSArray         *lngArray = [[NSBundle mainBundle] preferredLocalizations];
906    NSString        *lngKey, *name, *path;
907    NSURLRequest    *request;
908    NSURLDownload   *download;
909    NSFileManager   *fileManager = [NSFileManager defaultManager];
910    BOOL            isDir;
911
912    if ( rowIx < 0 )
913        return;
914
915    updateKey = [[[tableData dataDict] objectForKey:@"updateKey"] objectAtIndex:rowIx];
916    uDict = [updateDict objectForKey:updateKey];
917    names = [uDict objectForKey:@"i"];   // info file
918    //v    = [uDict objectForKey:@"v"];   // version
919    if ( !names )
920        return;
921    /* get language file */
922    lngKey = [[updateDict objectForKey:@"lng"] objectForKey:[lngArray objectAtIndex:0]];
923    if ( ! lngKey )
924        lngKey = @"gb";
925    if ( [names isKindOfClass:[NSDictionary class]] )
926        name = [names objectForKey:lngKey];
927    else
928        name = (NSString*)names;
929    //ext = [name pathExtension];
930    NSLog(@"name = %@", name);
931
932
933    url = [NSURL URLWithString:[NSString stringWithFormat:@"http://www.cenon.info/update/%@", name]];
934    path = vhfPathWithPathComponents(vhfUserLibrary(APPNAME), @".update", nil);
935    if ( ! [fileManager fileExistsAtPath:path isDirectory:&isDir] )
936        [fileManager createDirectoryAtPath:path attributes:nil];
937    else if ( ! isDir )
938        NSLog(@"Cenon-Update: unexpected file at path '%@'", path);
939    path = vhfPathWithPathComponents(path, name, nil);
940    request = [NSURLRequest requestWithURL:url];
941    if ( [[NSFileManager defaultManager] fileExistsAtPath:path] )
942        [self downloadDidFinish:(NSURLDownload*)request];
943    else
944    {   download = [[NSURLDownload alloc] initWithRequest:request delegate:self];
945        [download setDeletesFileUponFailure:YES];
946        [download setDestination:path allowOverwrite:YES];
947    }
948}
949
950@end
951
952
953/*
954 * Table Data for our tableView
955 */
956
957@implementation UpdateTableData
958
959+ (UpdateTableData*)tableData;
960{
961    return [[self new] autorelease];
962}
963
964- init
965{
966    [super init];
967    dataDict = [NSMutableDictionary new];
968    rowCount = 0;
969    return self;
970}
971
972- (int)numberOfRowsInTableView:(NSTableView*)table
973{
974    return rowCount;
975}
976
977- (id)tableView:(NSTableView*)table objectValueForTableColumn:(NSTableColumn*)column row:(int)row
978{   NSString    *obj = [[dataDict objectForKey:[column identifier]] objectAtIndex:row];
979
980    if ( [[column identifier] isEqual:@"install"] )
981    {
982        return ([obj intValue]) ? checkMarkStr : @"";
983    }
984    return obj;
985}
986
987- (void)setDataDict:(NSDictionary*)newDataDict
988{
989    [dataDict release];
990    dataDict = [newDataDict retain];
991    rowCount = [(NSArray*)[dataDict objectForKey:@"name"] count];
992}
993- (NSDictionary*)dataDict
994{
995    return dataDict;
996}
997
998- (void)dealloc
999{
1000    [dataDict release];
1001    [super dealloc];
1002}
1003
1004@end
1005