source: trunk/PrefsPane/src/FilterPublisher.m @ 1046

Revision 1046, 17.5 KB checked in by speck, 3 days ago (diff)

printf %d formatting fixes.
1.5b23

Line 
1/* Copyright (C) 2008-2009 Peter Speck
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#import "FilterPublisher.h"
18#import "GBDebug.h"
19#import "XmlDoc.h"
20#import "Xml.h"
21#import "ServerCommunication.h"
22#import "AlertHelper.h"
23#import "Prefs.h"
24#import "Filter.h"
25#import "FilterTabController.h"
26
27#define CURRENT_VERSION 3
28
29@interface FilterPublisher ()
30
31- (void)startWebDavPublish;
32
33- (void)hideProgressSheet;
34- (void)showProgressSheet;
35
36- (BOOL)validateSimpleUrl:(BOOL)verbose;
37
38- (NSString*)getMobileMeUsername;
39- (void)initMobileMeFilename;
40- (BOOL)validateMobileMe;
41
42@end
43
44@implementation FilterPublisher
45
46- (id)initWithFilterTabController:(FilterTabController*)filterTabController
47                       withFilter:(Filter*)filter
48                    usingMobileMe:(BOOL)mobileMe
49{
50    if (!(self = [super init]))
51        return nil;
52    NSAssert([NSBundle loadNibNamed: @"FilterPublisher" owner: self], @"Can't load nib 'FilterPublisher'");
53    NSAssert(urlSheet, @"urlSheet is null");
54    _filterTabController = [filterTabController retain];
55    _comm = [[filterTabController comm] retain];
56    _prefs = [[_comm prefs] retain];
57    _alertHelper = [[_prefs alertHelper] retain];
58    _filter = [filter retain];
59    _mobileMe = mobileMe;
60    [self retain]; // until we're done, released by [self dismiss].
61    //
62    if ([_filter url]) {
63        _url = [[NSURL URLWithString:[_filter url]] retain];
64        [self startWebDavPublish];
65        return self;
66    }
67    if (!_mobileMe) {
68        [urlTextField setDelegate:self];
69        [self validateSimpleUrl:NO];
70        [NSApp beginSheet: urlSheet
71           modalForWindow: [_alertHelper window]
72            modalDelegate: nil
73           didEndSelector: nil
74              contextInfo: nil];
75        return self;
76    }
77    [mmUsernameTextField setStringValue:[self getMobileMeUsername]];
78    [self initMobileMeFilename];
79    [self validateMobileMe];
80    [mmUsernameTextField setDelegate:self];
81    [mmFilenameTextField setDelegate:self];
82    [NSApp beginSheet: mobileMeSheet
83       modalForWindow: [_alertHelper window]
84        modalDelegate: nil
85       didEndSelector: nil
86          contextInfo: nil];
87    return self;
88}
89
90- (void)dealloc
91{
92    [self hideProgressSheet];
93    [self dismiss];
94    DebugNSLog(@"Publish.dealloc");
95    [_filterTabController release];
96    [_comm release];
97    [_url release];
98    [_filter release];
99    [_responseBody release];
100    [_alertHelper release];
101    [_prefs release];
102    [_comm release];
103    //
104    [urlSheet release];
105    [mobileMeSheet release];
106    [authSheet release];
107    [progressSheet release];
108    //
109    [super dealloc];
110}
111
112- (void)dismiss
113{
114    if (hasAutoReleasedSelf)
115        return;
116    DebugNSLog(@"dismiss.commit");
117    hasAutoReleasedSelf = YES;
118    [_connection cancel];
119    [self autorelease];
120}
121
122#pragma mark ------------------------ MobileMe
123
124static NSString* mobileMeUsername;
125- (NSString*)getMobileMeUsername
126{
127    if (mobileMeUsername)
128        return mobileMeUsername;
129    ICInstance icInstance;
130    if (ICStart(&icInstance, 0x56694762) == noErr) {
131        Str255 key;
132        CFStringGetPascalString((CFStringRef)@"IToolsAccountName", key, sizeof(key) - 1, kCFStringEncodingMacRoman);
133        Handle h = NewHandle(256);
134        if (!h) {
135            NSLog(@"Can't allocate 256 bytes handle!");
136        } else {
137            ICAttr itemAttributes;
138            OSStatus error = ICFindPrefHandle(icInstance, key, &itemAttributes, h);
139            if (error == noErr) {
140                HLock(h);
141                mobileMeUsername = (NSString*)CFStringCreateWithPascalString(NULL, (ConstStr255Param)*h, kCFStringEncodingMacRoman);
142                HUnlock(h);
143                NSLog(@"Got MobileMe username from ICC: %@", mobileMeUsername);
144            } else {
145                if (error == icPrefNotFoundErr)
146                    NSLog(@"User missing MobileMe username.");
147                else
148                    NSLog(@"Got error %ld from ICConfig for MobileMe username", (long)error);
149            }
150            DisposeHandle(h);
151        }
152        ICEnd(icInstance);
153    }
154    if (![mobileMeUsername length])
155        mobileMeUsername = [NSUserName() retain];
156    return mobileMeUsername;
157}
158
159- (void)initMobileMeFilename
160{
161    NSString* s = [_filter name];
162    NSMutableString* sb = [NSMutableString stringWithCapacity:[s length]];
163    for (NSUInteger i = 0; i < [s length]; i++) {
164        unichar ch = [s characterAtIndex:i];
165        if (ch >= 'A' && ch <= 'Z') {
166            ch += 'a' - 'A';
167        } else if (ch == ' ') {
168            if (![sb length] || [sb characterAtIndex:[sb length] - 1] == '-')
169                continue;
170            ch = '-';
171        } else if ((ch >= '0' && ch <= '9')
172                   || (ch >= 'a' && ch <= 'z')
173                   || ch == '-' || ch == '_'
174                   || (ch == '.' && [sb length])) {
175            // as-is
176        } else {
177            continue;
178        }
179        [sb appendFormat:@"%c", ch]; // very advanced library.
180    }
181    while ([sb hasSuffix:@"."] || [sb hasSuffix:@"-"])
182        [sb deleteCharactersInRange:NSMakeRange([sb length] - 1, 1)];
183    [sb appendString:@".xml"];
184    [mmFilenameTextField setStringValue:sb];
185}
186
187- (BOOL)validateMobileMeName:(NSString*)s isUsername:(BOOL)isUsername labelTextField:(NSTextField*)label
188{
189    NSString* error = NULL;
190    for (NSUInteger i = 0; i < [s length]; i++) {
191        unichar ch = [s characterAtIndex:i];
192        if (ch == '.') {
193            if (i == 0)
194                error = @"First character must not be '.'";
195            else if (i == [s length] - 1)
196                error = @"Last character must not be '.'";
197            else
198                continue;
199            break;
200        }
201        if (ch >= '0' && ch <= '9')
202            continue;
203        if (ch >= 'a' && ch <= 'z')
204            continue;
205        if (ch >= 'A' && ch <= 'Z')
206            continue;
207        if (ch == '-' && !isUsername)
208            continue;
209        if (ch == '_')
210            continue;
211        if (ch == ' ')
212            error = @"Spaces are not allowed. Use a dash instead.";
213        else
214            error = [NSString stringWithFormat:@"Invalid character: %c", ch];
215        break;
216    }
217    if (!error && !isUsername && ![s hasSuffix:@".xml"]) {
218        error = @"Filename must end with '.xml'";
219    }
220    if (error)
221        [mmErrorLabel setStringValue:error];
222    [label setTextColor:(error ? [mmErrorLabel textColor] : [NSColor blackColor])];
223    return !error;
224}
225
226- (BOOL)validateMobileMe
227{
228    BOOL accepted = [self validateMobileMeName:[mmUsernameTextField stringValue] isUsername:YES labelTextField:mmUsernameLabel]
229    /**/    && [self validateMobileMeName:[mmFilenameTextField stringValue] isUsername:NO labelTextField:mmFilenameLabel];
230    [mmErrorLabel setHidden:accepted];
231    [mmOkBtn setEnabled:accepted];
232    return accepted;
233}
234
235#pragma mark ------------------------ simple URL
236
237- (BOOL)validateSimpleUrl:(BOOL)verbose
238{
239    NSString* s = [urlTextField stringValue];
240    NSString* error = NULL;
241    if (![s length]) {
242        error = @" ";
243    } else if (![s hasPrefix:@"http://"] && ![s hasPrefix:@"https://"]) {
244        error = @"URL must start with  http://  or  https://";
245    } else if (![s hasSuffix:@".xml"]) {
246        error = @"URL must end with  .xml";
247    } else {
248        NSString* host = [Filter extractHostFromStringUrl:s];
249        if (![host length])
250            error = @"URL has invalid hostname";
251        else if (verbose && ([host isEqual:@"example.com"] || [host hasSuffix:@".example.com"]))
252            error = @"example.com is not valid as host.";
253    }
254    if (!error) {
255        @try {
256            if (![NSURL URLWithString:s])
257                error = @"Invalid URL (NSURL failed)";
258        }
259        @catch (NSException* ex) {
260            error = @"Invalid URL (NSURL exception)";
261        }
262    }
263    if (error)
264        [urlErrorLabel setStringValue:error];
265    [urlErrorLabel setHidden:!error];
266    [urlOkBtn setEnabled:!error];
267    return !error;
268}
269
270- (void)controlTextDidChange:(NSNotification *)aNotification
271{
272    //DebugNSLog(@"controlTextDidChange: %@", aNotification);
273    if (_mobileMe)
274        [self validateMobileMe];
275    else
276        [self validateSimpleUrl:NO];
277}
278
279#pragma mark ------------------------ progress
280- (void)hideProgressSheet
281{
282    if (!_showsProgressSheet)
283        return;
284    _showsProgressSheet = NO;
285    [NSApp endSheet:progressSheet];
286    [progressSheet orderOut:self];
287}
288
289- (void)showProgressSheet
290{
291    if (_showsProgressSheet || hasAutoReleasedSelf)
292        return;
293    _showsProgressSheet = YES;
294    [NSApp beginSheet: progressSheet
295       modalForWindow: [_alertHelper window]
296        modalDelegate: nil
297       didEndSelector: nil
298          contextInfo: nil];
299    [processIndicator startAnimation:self];
300}
301
302- (IBAction)uploadCancelAction:(id)sender
303{
304    [self dismiss];
305}
306
307#pragma mark ------------------------ url sheet
308
309- (IBAction)publishCancelAction:(id)sender
310{
311    DebugNSLog(@"FilterPublisher.publishCancelAction");
312    _suppressUserFeedback = YES;
313    if (_mobileMe) {
314        [NSApp endSheet:mobileMeSheet];
315        [mobileMeSheet orderOut:self];
316    } else {
317        [NSApp endSheet:urlSheet];
318        [urlSheet orderOut:self];
319    }
320    [self dismiss];
321}
322
323- (IBAction)publishUrlConfirmAction:(id)sender
324{
325    DebugNSLog(@"FilterPublisher.publishUrlConfirmAction");
326    if (![self validateSimpleUrl:YES])
327        return;
328    [NSApp endSheet:urlSheet];
329    [urlSheet orderOut:self];
330    @try {
331        _url = [[NSURL URLWithString:[urlTextField stringValue]] retain];
332    }
333    @catch (NSException* ex) {
334        NSLog(@"Got exception in publishConfirmAction: %@", ex);
335        [_alertHelper prepareCriticalAlertWithTitle:@"Invalid URL" withInformativeText:NULL];
336        [_alertHelper showPreparedAlert];
337        return;
338    }
339    NSString* u = [_url absoluteString];
340    for (Filter* filter in [_filterTabController filterArray]) {
341        if (![u isEqual:[filter url]])
342            continue;
343        NSString* msg = [filter isSubscription]
344        ? @"Cannot publish filter with same URL as a subscription as it would overwrite the subscription original."
345        : @"Cannot publish filter with same URL as another published filter as it would overwrite the other published filter.";
346        [_alertHelper prepareCriticalAlertWithTitle:@"Duplicate URL" withInformativeText:msg];
347        [_alertHelper showPreparedAlert];
348        return;
349    }
350    [self startWebDavPublish];
351}
352
353- (IBAction)publishMobileMeConfirmAction:(id)sender
354{
355    DebugNSLog(@"FilterPublisher.publishMobileMeConfirmAction");
356    if (![self validateMobileMe]) {
357        NSLog(@"Got publishMobileMeConfirmAction with non-valid MobileMe info!");
358        return;
359    }
360    [NSApp endSheet:mobileMeSheet];
361    [mobileMeSheet orderOut:self];
362    NSString* s = [NSString stringWithFormat:@"http://idisk.mac.com/%@/%@/%@",
363                   [mmUsernameTextField stringValue],
364                   ([mmFolderMatrix selectedRow] ? @"Documents" : @"Public"),
365                   [mmFilenameTextField stringValue]];
366    NSLog(@"MobileMe URL: %@", s);
367    @try {
368        _url = [[NSURL URLWithString:s] retain];
369    }
370    @catch (NSException* ex) {
371        NSLog(@"Got exception in publishConfirmAction: %@", ex);
372        [_alertHelper prepareCriticalAlertWithTitle:@"Invalid URL" withInformativeText:NULL];
373        [_alertHelper showPreparedAlert];
374        return;
375    }
376    [self startWebDavPublish];
377}
378
379
380#pragma mark ------------------------ start uploading
381- (void)startWebDavPublish
382{
383    DebugNSLog(@"Publish.start");
384    NSXMLElement *filterDataE = [[[_filter filterDataE] copy] autorelease];
385    NSString* comments = [_filter comments];
386    if ([comments length]) {
387        [Xml setAttribute:comments withName:@"description" inElement:filterDataE]; // storing in attribute is old format.
388        [Xml setCData:comments forXPath:@"description" withRoot:filterDataE];
389    }
390    [Xml setAttribute:[NSString stringWithFormat:@"%d", CURRENT_VERSION]
391             withName:@"format-version"
392            inElement:filterDataE];
393    [Xml setAttribute:[[[_filterTabController bundle] infoDictionary] objectForKey:@"CFBundleVersion"]
394             withName:@"gb-version"
395            inElement:filterDataE];
396    XmlDoc* doc = [[[XmlDoc alloc] initWithRootElement:filterDataE] autorelease];
397    NSData* data = [doc printToData];
398    NSLog(@"Publishes filter, %ld bytes", (long)[data length]);
399    //DebugNSLog(@"XML: %@", [doc printToString]);
400    NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:_url
401                                                       cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
402                                                   timeoutInterval:60.0];
403    [req setHTTPMethod: @"PUT"];
404    [req setValue:@"application/xml" forHTTPHeaderField:@"Content-type"];
405    [req setHTTPBody:data];
406    _responseBody = [[NSMutableData dataWithCapacity:0] retain];
407    [self showProgressSheet];
408    _connection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
409    if (_connection)
410        return;
411    [_alertHelper prepareCriticalAlertWithTitle:@"Failed to publish"
412                            withInformativeText:@"unknown error: NSURLConnection.initWithRequest returned null"];
413    [_alertHelper showPreparedAlert];
414    [self dismiss];
415}
416
417#pragma mark ------------------------ authentication
418
419- (IBAction)authCancelAction:(id)sender
420{
421    DebugNSLog(@"Publish.authCancelAction");
422    _suppressUserFeedback = YES;
423    [NSApp endSheet:authSheet];
424    [authSheet orderOut:self];
425    [[_authChallenge sender] cancelAuthenticationChallenge:_authChallenge];
426    [_authChallenge release];
427    [self dismiss];
428}
429
430- (IBAction)authConfirmAction:(id)sender
431{
432    DebugNSLog(@"Publish.authConfirmAction");
433    [NSApp endSheet:authSheet];
434    [authSheet orderOut:self];
435    [self showProgressSheet];
436    NSURLCredentialPersistence persistence = [authRememberCB state]
437    /**/ ? NSURLCredentialPersistencePermanent : NSURLCredentialPersistenceForSession;
438    NSURLCredential *credential = [NSURLCredential credentialWithUser:[authNameTextField stringValue]
439                                                             password:[authPasswordTextField stringValue]
440                                                          persistence:persistence];
441    DebugNSLog(@"  Uses auth: %@", credential);
442    [[_authChallenge sender] useCredential:credential forAuthenticationChallenge:_authChallenge];
443    [_authChallenge autorelease];
444    _authChallenge = NULL;
445}
446
447
448-(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
449{
450    NSLog(@"Publish.didReceiveAuthenticationChallenge: %@", challenge);
451    NSURLCredential* credential = [challenge proposedCredential];
452    if (credential && [credential hasPassword] && ![challenge previousFailureCount]) {
453        [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
454        return;
455    }
456    NSURLProtectionSpace* space = [challenge protectionSpace];
457    NSString* msg = @"";
458    if ([challenge previousFailureCount])
459        msg = @"Previously specified username/password was NOT accepted.\n\n";
460    msg = [NSString stringWithFormat:@"%@Enter your name and password to publish the filter at the URL:\n   %@\n",
461                                   msg, [_url absoluteURL]];
462    if ([[space realm] length])
463        msg = [NSString stringWithFormat:@"%@in the realm \"%@\".\n", msg, [space realm]];
464    else
465        msg = [msg stringByAppendingString:@"\n"];
466    if ([[space authenticationMethod] isEqual:NSURLAuthenticationMethodHTTPDigest])
467        msg = [msg stringByAppendingString:@"Your name and password will be sent securely."];
468    else
469        msg = [msg stringByAppendingString:@"Your name and password will be send in plain text and NOT securely."];
470    [authMessageTextField setStringValue:msg];
471    NSString* s = [credential user];
472    [authNameTextField setStringValue:[s length] ? s : @""];
473    [authPasswordTextField setStringValue:@""];
474    [authRememberCB setState:!credential || ([credential persistence] == NSURLCredentialPersistencePermanent)];
475    [self hideProgressSheet];
476    [NSApp beginSheet: authSheet
477       modalForWindow: [_alertHelper window]
478        modalDelegate: nil
479       didEndSelector: nil
480          contextInfo: nil];
481    _authChallenge = [challenge retain];
482}
483
484#ifdef DEBUG_XXX
485- (BOOL)respondsToSelector:(SEL)aSelector
486{
487    BOOL rts = [super respondsToSelector:aSelector];
488    DebugNSLog(@"FilterPublisher, %@  respondsToSelector  %s", (rts ? @"does" : @"does not"), aSelector);
489    return rts;
490}
491#endif
492
493#pragma mark ------------------------ misc
494
495- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
496{
497    // called for each response, i.e. for each redirect
498    [_responseBody setLength:0];
499    NSHTTPURLResponse* http = (NSHTTPURLResponse*)response;
500    _lastHttpStatusCode = [http statusCode];
501    DebugNSLog(@"didReceiveResponse: %ld   %@", _lastHttpStatusCode, [http allHeaderFields]);
502}
503
504- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
505{
506    [_responseBody appendData:data];
507}
508
509- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
510{
511    NSLog(@"Publish subscription request failed (suppress = %d): %@", _suppressUserFeedback, error);
512    if (_suppressUserFeedback) {
513        [self dismiss];
514        return;
515    }
516    NSString* a = [error localizedDescription];
517    NSString* b = [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey];
518    NSString* updateError = [NSString stringWithFormat:@"%@ %@", a, b];
519    [self hideProgressSheet];
520    [_alertHelper prepareCriticalAlertWithTitle:@"Failed publishing filter" withInformativeText:updateError];
521    [_alertHelper showPreparedAlert];
522    [self dismiss];
523}
524
525- (void)handlePublishSuccess
526{
527    DebugNSLog(@"Publish success.");
528    if (![_filter url]) {
529        DebugNSLog(@"Updates filter with published URL.");
530        [_filter setUrl:[_url absoluteString]];
531    }
532    [_filter notifyPublished];
533    //
534    [_filter readAttributesFromXml];
535    [_prefs markAsDirty];
536    [self dismiss];
537}
538
539- (void)connectionDidFinishLoading:(NSURLConnection *)connection
540{
541    DebugNSLog(@"connectionDidFinishLoading, http status = %ld, suppress = %d", _lastHttpStatusCode, _suppressUserFeedback);
542    DebugNSLog(@"Publish response: %@",
543               [[[NSString alloc] initWithData:_responseBody encoding:NSISOLatin1StringEncoding] autorelease]);
544    [self hideProgressSheet];
545    if (_lastHttpStatusCode == 200 || _lastHttpStatusCode == 201 || _lastHttpStatusCode == 204) {
546        // Servers should respond with HTTP_NO_CONTENT = 204
547        // but some responds with HTTP_CREATED = 201 or HTTP_OK = 200
548        [self handlePublishSuccess];
549        return;
550    }
551    [self dismiss];
552    if (_suppressUserFeedback)
553        return;
554    NSString* msg = [NSHTTPURLResponse localizedStringForStatusCode:_lastHttpStatusCode];
555    if (_lastHttpStatusCode == 405)
556        msg = [msg stringByAppendingString:@"\n\nThis typically means that the web server doesn't support WebDav."];
557    NSString* s = [NSString stringWithFormat:@"Got http error %d: %@", _lastHttpStatusCode, msg];
558    [_alertHelper prepareCriticalAlertWithTitle:@"Failed publishing filter" withInformativeText:s];
559    [_alertHelper showPreparedAlert];
560    DebugNSLog(@"Ended with error alert: %@", s);
561}
562
563@end
Note: See TracBrowser for help on using the repository browser.