1// GalleryUploader.mm
2// this file is part of Context Free
3// ---------------------
4// Copyright (C) 2007 Mark Lentczner - markl@glyphic.com
5// Copyright (C) 2008-2012 John Horigan - john@glyphic.com
6//
7// This program is free software; you can redistribute it and/or
8// modify it under the terms of the GNU General Public License
9// as published by the Free Software Foundation; either version 2
10// of the License, or (at your option) any later version.
11//
12// This program is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15// GNU General Public License for more details.
16//
17// You should have received a copy of the GNU General Public License
18// along with this program; if not, write to the Free Software
19// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
20//
21// John Horigan can be contacted at john@glyphic.com or at
22// John Horigan, 1209 Villa St., Mountain View, CA 94041-1123, USA
23//
24// Mark Lentczner can be contacted at markl@glyphic.com or at
25// Mark Lentczner, 1209 Villa St., Mountain View, CA 94041-1123, USA
26//
27//
28
29#import "GalleryUploader.h"
30#import "CFDGDocument.h"
31#import "GView.h"
32#import "VariationFormatter.h"
33#import <WebKit/WebFrame.h>
34#import <WebKit/DOMCore.h>
35#include <Security/Security.h>
36
37#include <string>
38#include <sstream>
39
40#include "upload.h"
41
42namespace {
43
44    std::string asString(NSString* ns)
45    {
46        NSData* d = [ns dataUsingEncoding: NSUTF8StringEncoding];
47        std::string s(reinterpret_cast<const char*>([d bytes]), [d length]);
48        return s;
49    }
50
51    NSString* asNSString(const std::string& s)
52    {
53        return [NSString stringWithUTF8String: s.c_str()];
54    }
55
56    static NSString* ccURI   = @"CreativeCommonsLicenseURI";
57    static NSString* ccName  = @"CreativeCommonsLicenseName";
58    static NSString* ccImage = @"CreativeCommonsLicenseImage";
59
60    static NSString* galDomain = @"www.contextfreeart.org";
61    static NSString* galPath = @"/gallery/";
62
63#if 1
64    static NSString* uploadUrl =
65        @"https://www.contextfreeart.org/gallery/upload.php";
66
67    static NSString* displayUrl =
68        @"https://www.contextfreeart.org/gallery/view.php?id=%d";
69
70    static NSString* tagsUrl =
71        @"https://www.contextfreeart.org/gallery/tags.php?t=tags";
72#else
73    static NSString* uploadUrl =
74        @"http://localhost/~john/cfa2/gallery/upload.php";
75
76    static NSString* displayUrl =
77        //@"http://localhost:8000/main.html#design/%d";
78        @"http://localhost/~john/cfa2/gallery/view.php?id=%d";
79
80    static NSString* tagsUrl =
81        @"http://localhost/~john/cfa2/gallery/tags.php?t=tags";
82#endif
83
84    SecKeychainItemRef getGalleryKeychainItem(NSString* name)
85    {
86        SecKeychainItemRef itemRef = nil;
87        if (SecKeychainFindInternetPassword(NULL,
88                                            (UInt32)[galDomain length],
89                                            [galDomain UTF8String],
90                                            0,
91                                            NULL,
92                                            (UInt32)[name length],
93                                            [name UTF8String],
94                                            (UInt32)[galPath length],
95                                            [galPath UTF8String],
96                                            0,
97                                            kSecProtocolTypeHTTPS,
98                                            kSecAuthenticationTypeHTMLForm,
99                                            0,
100                                            NULL,
101                                            &itemRef
102                                            ) == errSecSuccess)
103        {
104            return itemRef;
105        }
106        return nil;
107    }
108}
109
110
111
112@implementation GalleryUploader
113
114+ (NSString*) copyPassword:(NSString *)forUser
115{
116    NSString* ret = NULL;
117
118    if (SecKeychainItemRef itemRef = getGalleryKeychainItem(forUser)) {
119        SecKeychainAttribute     attr;
120        SecKeychainAttributeList attrList;
121        UInt32                   length;
122        void                     *outData;
123
124        // To set the account name attribute
125        attr.tag = kSecAccountItemAttr;
126        attr.length = 0;
127        attr.data = NULL;
128        attrList.count = 1;
129        attrList.attr = &attr;
130
131
132        if (SecKeychainItemCopyContent(itemRef, NULL, &attrList, &length, &outData) == noErr) {
133            ret = [[NSString alloc] initWithBytes:outData
134                                           length:length
135                                         encoding:NSUTF8StringEncoding];
136            SecKeychainItemFreeContent(&attrList, outData);
137        }
138        CFRelease(itemRef);
139    }
140    return ret;
141}
142
143+ (void) savePassword:(NSString*)password forUser:(NSString*)user
144{
145    SecKeychainItemRef itemRef;
146
147    if (!password || !user) return;
148
149    if (SecKeychainItemRef itemRef = getGalleryKeychainItem(user)) {
150        // Try to update password of existing keychain item
151        SecKeychainAttribute     attr;
152        SecKeychainAttributeList attrList;
153
154        // To set the account name attribute
155        attr.tag = kSecAccountItemAttr;
156        attr.length = (UInt32)[user length];
157        attr.data = (void*)[user UTF8String];
158        attrList.count = 1;
159        attrList.attr = &attr;
160
161        if (SecKeychainItemModifyContent(itemRef, &attrList, (UInt32)[password length],
162                                         (void *)[password UTF8String]) == noErr)
163        {
164            return;         // success, we're done
165        }
166        // failure, make a new item
167    }
168
169    if (OSStatus s = SecKeychainAddInternetPassword(NULL,
170                                                   (UInt32)[galDomain length],
171                                                   [galDomain UTF8String],
172                                                   0,
173                                                   NULL,
174                                                   (UInt32)[user length],
175                                                   [user UTF8String],
176                                                   (UInt32)[galPath length],
177                                                   [galPath UTF8String],
178                                                   0,
179                                                   kSecProtocolTypeHTTPS,
180                                                   kSecAuthenticationTypeHTMLForm,
181                                                   (UInt32)[password length],
182                                                   [password UTF8String],
183                                                   &itemRef
184                                                   ) != noErr)
185    {
186        CFStringRef msg = SecCopyErrorMessageString(s, NULL);
187        NSLog(@"Error saving password: %@", (NSString*)msg);
188        CFRelease(msg);
189    }
190}
191
192- (id)initForDocument:(CFDGDocument*)document andView:(GView*)view
193{
194    self = [self initWithWindowNibName: @"GalleryUploader"];
195    if (!self) return self;
196
197    mDocument = document;
198    mView = view;
199    mStatus = 0;
200
201        // no need to retain - the document is retaining us!
202
203    [self loadWindow];
204    [mFormView retain];
205    [mProgressView retain];
206    [mDoneView retain];
207    return self;
208}
209
210- (void) dealloc {
211    [mDoneView release];
212    [mProgressView release];
213    [mFormView release];
214    if (mTagsTask) {
215        if ([mTagsTask state] == NSURLSessionTaskStateRunning)
216            [mTagsTask cancel];
217        [mTagsTask release];
218    }
219    if (mUploadTask) {
220        if ([mUploadTask state] == NSURLSessionTaskStateRunning)
221            [mUploadTask cancel];
222        [mUploadTask release];
223    }
224    [mOrigPassword release];
225    [mOrigName release];
226    [mTags release];
227    [super dealloc];
228}
229
230
231- (NSData*)requestBody
232{
233    Upload upload;
234
235    upload.mUserName    = asString([mUserNameField stringValue]);
236    upload.mPassword    = asString([mPasswordField stringValue]);
237    upload.mTitle       = asString([mTitleField stringValue]);
238    upload.mNotes       = asString([mNotesView string]);
239    upload.mTags        = asString([[mTagsView objectValue] componentsJoinedByString: @" "]);
240    upload.mFileName    = asString([mFileField stringValue]) + ".cfdg";
241    upload.mVariation   = [mView variation];
242    upload.mTiled       = 0;
243    if ([mTiled state] == NSOnState) {
244        upload.mTiled = [mView isFrieze];
245        if ([mView isTiled] && !upload.mTiled)
246            upload.mTiled = 3;
247    }
248    upload.mCompression = (Upload::Compression)
249                    [[mCompressionMatrix selectedCell] tag];
250    upload.mccLicenseURI    = asString(mDefccURI);
251    upload.mccLicenseName   = asString(mDefccName);
252    upload.mccLicenseImage  = asString(mDefccImage);
253
254    BOOL crop = upload.mTiled ||
255            [[NSUserDefaults standardUserDefaults] boolForKey: @"SaveCropped"];
256
257    NSSize mult = NSMakeSize([mSaveTileWidth floatValue], [mSaveTileHeight floatValue]);
258
259    NSData* textData    = [mDocument getContent];
260    NSData* imageData   = [mView pngImageDataCropped: crop
261                                          multiplier: [mView isTiled] ? &mult : nil];
262
263    if (!imageData) return nil;
264
265    upload.mText        = reinterpret_cast<const char*>([textData bytes]);
266    upload.mTextLen     = [textData length];
267    upload.mImage       = reinterpret_cast<const char*>([imageData bytes]);
268    upload.mImageLen    = [imageData length];
269
270    std::ostringstream design;
271    upload.generatePayload(design);
272
273    return [NSData dataWithBytes: design.str().data()
274                    length: design.str().length()];
275}
276
277- (void)setView:(NSView*)view
278{
279    if (view == mProgressView)
280        [mProgressBar startAnimation: nil];
281    else
282        [mProgressBar stopAnimation: nil];
283
284    NSRect contentFrame = [mContentView frame];
285
286    NSSize oldContentSize = contentFrame.size;
287    NSSize newContentSize = [view frame].size;
288    NSSize deltaSize;
289    deltaSize.width = newContentSize.width - oldContentSize.width;
290    deltaSize.height = newContentSize.height - oldContentSize.height;
291
292    NSView* container = [mContentView superview];
293    [mContentView removeFromSuperview];
294    contentFrame.size = newContentSize;
295    [view setFrame: contentFrame];
296    [container addSubview: view];
297    mContentView = view;
298
299    NSWindow* window = [self window];
300    NSRect f = [window frame];
301    f.origin.x -= deltaSize.width / 2.0;
302    f.origin.y -= deltaSize.height;
303    f.size.width += deltaSize.width;
304    f.size.height += deltaSize.height;
305
306    [window setFrame: f display: YES animate: YES];
307}
308
309
310- (IBAction)retry:(id)sender
311{
312    if (mSuccessId) {
313        NSString* where = [NSString stringWithFormat: displayUrl, mSuccessId];
314        [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: where]];
315        [self cancel: sender];
316    } else {
317        [self setView: mFormView];
318    }
319}
320
321- (IBAction)show:(id)sender
322{
323    [self setView: mFormView];
324
325    NSMutableURLRequest* request =
326    [NSMutableURLRequest requestWithURL: [NSURL URLWithString: tagsUrl]
327                            cachePolicy: NSURLRequestReloadIgnoringCacheData
328                        timeoutInterval: 120.0
329     ];
330    [request setHTTPMethod: @"GET"];
331    [request setValue: @"application/json"
332        forHTTPHeaderField: @"Content-Type"];
333
334    mTagsTask = [[NSURLSession sharedSession] dataTaskWithRequest: request
335                                                completionHandler:^(NSData *data,
336                                                                    NSURLResponse *response,
337                                                                    NSError *error)
338                    {
339                        if (data) {
340                            auto tags = Upload::AllTags(static_cast<const char*>([data bytes]),
341                                                        static_cast<std::size_t>([data length]));
342                            NSMutableArray<NSString*>* tagset = [NSMutableArray arrayWithCapacity: tags.size()];
343                            for (auto&& tag: tags)
344                                [tagset addObject: [NSString stringWithUTF8String:tag.c_str()]];
345                            [tagset sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
346                            dispatch_async(dispatch_get_main_queue(), ^{mTags = [tagset retain];});
347                        }
348                    }];
349    [mTagsTask retain];
350    [mTagsTask resume];
351
352    mOrigName = [[NSString alloc] initWithString: [mUserNameField stringValue]];
353    mOrigPassword = [GalleryUploader copyPassword: mOrigName];
354    if (mOrigPassword)
355        [mPasswordField setStringValue: mOrigPassword];
356
357
358    if ([[mTitleField stringValue] length] == 0) {
359        NSString* name = [mDocument displayName];
360        name = name.stringByDeletingPathExtension;
361        name = name.capitalizedString;
362        [mTitleField setStringValue: name];
363    }
364
365    if ([[mFileField stringValue] length] == 0) {
366        NSString* file = [[mDocument fileURL] path];
367        if (file) {
368            file = [[file stringByDeletingPathExtension] lastPathComponent];
369        } else {
370            file = [mDocument displayName];
371        }
372        [mFileField setStringValue: file];
373    }
374
375    [mVariationField setIntValue: [mView variation]];
376
377    int bestCompression = [mView canvasColor256] ?
378        Upload::CompressPNG8 : Upload::CompressJPEG;
379    [mCompressionMatrix selectCellWithTag: bestCompression];
380
381    bool tiled = [mView isTiled];
382    [mTiled setEnabled: tiled];
383    [mCropCheck setEnabled: !tiled];
384    [mSaveTileWidth setHidden: !tiled];
385    [mSaveTileHeight setHidden: !tiled];
386    [mWidthLabel setHidden: !tiled];
387    [mHeightLabel setHidden: !tiled];
388    [mMultLabel setHidden: !tiled];
389    [mTiled setState: (tiled ? NSOnState : NSOffState)];
390
391    if (tiled) {
392        CFDG::frieze_t frz = (CFDG::frieze_t)[mView isFrieze];
393        [mSaveTileWidth setEnabled: frz != CFDG::frieze_y];
394        [mSaveTileHeight setEnabled: frz != CFDG::frieze_x];
395
396        NSNumber* one = [NSNumber numberWithInt: 1];
397        NSNumber* hundred = [NSNumber numberWithInt: 100];
398        NSNumberFormatter* fmt = [mSaveTileWidth formatter];
399        [fmt setMinimum: one];
400        [fmt setMaximum: hundred];
401        fmt = [mSaveTileHeight formatter];
402        [fmt setMinimum: one];
403        [fmt setMaximum: hundred];
404        [mSaveTileWidth setFloatValue: 1.0];
405        [mSaveTileHeight setFloatValue: 1.0];
406    }
407
408    [self updateCCInfo: YES];
409
410    [NSApp beginSheet: [self window]
411        modalForWindow: [mView window]
412        modalDelegate: nil didEndSelector: nil contextInfo: nil];
413}
414
415
416- (void)updateCCInfo:(BOOL)reset
417{
418    if (reset) {
419        mDefccURI = [[NSUserDefaults standardUserDefaults] stringForKey: ccURI];
420        mDefccName = [[NSUserDefaults standardUserDefaults] stringForKey: ccName];
421        mDefccImage = [[NSUserDefaults standardUserDefaults] stringForKey: ccImage];
422    }
423
424    if (!mDefccURI || !mDefccName || !mDefccImage || [mDefccURI length] == 0 ||
425        [mDefccName length] == 0 || [mDefccImage length] == 0)
426    {
427        mDefccURI = @"";
428        mDefccName = @"no license chosen";
429        mDefccImage = @"";
430    }
431
432    bool isSeeded = [mDefccURI length] > 0;
433
434    [mLicenseName setStringValue: mDefccName];
435
436    if (isSeeded) {
437        NSURL* iconURL = [NSURL URLWithString: mDefccImage];
438        NSImage* icon = [[[NSImage alloc] initWithContentsOfURL: iconURL] autorelease];
439        [mLicenseImage setImage: icon];
440        [mLicenseImage setEnabled: YES];
441    } else {
442        [mLicenseImage setImage: nil];
443        [mLicenseImage setEnabled: NO];
444    }
445}
446
447- (IBAction)goToCreateAccount:(id)sender
448{
449    NSURL* url = [NSURL URLWithString:
450        @"https://www.contextfreeart.org/phpbb/ucp.php?mode=register"];
451
452    [[NSWorkspace sharedWorkspace] openURL: url];
453}
454
455- (IBAction)changeLicense:(id)sender
456{
457    NSPopUpButton* ccmenu = (NSPopUpButton*)sender;
458    NSInteger tag = [ccmenu selectedTag];
459    switch (tag) {
460        case 0:
461            [self updateCCInfo: YES];
462            return;
463        case 1:
464            mDefccURI = @"https://creativecommons.org/publicdomain/zero/1.0/";
465            mDefccName = @"CC0 1.0 Universal (CC0 1.0) Public Domain Dedication";
466            mDefccImage = @"https://licensebuttons.net/p/zero/1.0/88x31.png";
467            break;
468        case 2:
469            mDefccURI = @"https://creativecommons.org/licenses/by/4.0/";
470            mDefccImage = @"https://licensebuttons.net/l/by/4.0/88x31.png";
471            mDefccName = @"Creative Commons Attribution 4.0 International";
472            break;
473        case 3:
474            mDefccURI = @"https://creativecommons.org/licenses/by-sa/4.0/";
475            mDefccImage = @"https://licensebuttons.net/l/by-sa/4.0/88x31.png";
476            mDefccName = @"Creative Commons Attribution-ShareAlike 4.0 International";
477            break;
478        case 4:
479            mDefccURI = @"https://creativecommons.org/licenses/by-nd/4.0/";
480            mDefccImage = @"https://licensebuttons.net/l/by-nd/4.0/88x31.png";
481            mDefccName = @"Creative Commons Attribution-NoDerivatives 4.0 International";
482            break;
483        case 5:
484            mDefccURI = @"https://creativecommons.org/licenses/by-nc/4.0/";
485            mDefccImage = @"https://licensebuttons.net/l/by-nc/4.0/88x31.png";
486            mDefccName = @"Creative Commons Attribution-NonCommercial 4.0 International";
487            break;
488        case 6:
489            mDefccURI = @"https://creativecommons.org/licenses/by-nc-sa/4.0/";
490            mDefccImage = @"https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png";
491            mDefccName = @"Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International";
492            break;
493        case 7:
494            mDefccURI = @"https://creativecommons.org/licenses/by-nc-nd/4.0/";
495            mDefccImage = @"https://licensebuttons.net/l/by-nc-nd/4.0/88x31.png";
496            mDefccName = @"Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International";
497            break;
498        default:
499            mDefccURI = @"";
500            mDefccName = @"no license chosen";
501            mDefccImage = @"";
502            break;
503    }
504    [self updateCCInfo: NO];
505}
506
507- (IBAction)licenseDetails:(id)sender
508{
509    NSURL* url = [NSURL URLWithString: mDefccURI];
510
511    [[NSWorkspace sharedWorkspace] openURL: url];
512}
513
514- (IBAction)upload:(id)sender
515{
516    if (![[mPasswordField stringValue] isEqualToString: mOrigPassword] ||
517        ![[mUserNameField stringValue] isEqualToString: mOrigName])
518    {
519        [GalleryUploader savePassword: [mPasswordField stringValue]
520                              forUser: [mUserNameField stringValue]];
521    }
522
523    NSData* body = [self requestBody];
524    if (!body) {
525        [mMessage setString: @"Failed to generate PNG image to upload."];
526        [self setView: mDoneView];
527        return;
528    }
529
530    NSMutableURLRequest* request =
531        [NSMutableURLRequest requestWithURL: [NSURL URLWithString: uploadUrl]
532                                cachePolicy: NSURLRequestReloadIgnoringCacheData
533                            timeoutInterval: 120.0
534        ];
535    [request setHTTPMethod: @"POST"];
536    [request setValue: asNSString(Upload::generateContentType())
537                forHTTPHeaderField: @"Content-Type"];
538
539    mUploadTask = [[NSURLSession sharedSession] uploadTaskWithRequest: request
540                                                             fromData: body
541                                                    completionHandler:^(NSData *data,
542                                                                        NSURLResponse *response,
543                                                                        NSError *error)
544    {
545        NSString* newmsg = nil;
546        BOOL retry = NO;
547        unsigned design = 0;
548        NSAttributedString *theParsedHTML = nil;
549        if (error) {
550            newmsg = [error localizedDescription];
551        } else {
552            NSInteger status = 0;
553            if ([response isKindOfClass:[NSHTTPURLResponse class]] ) {
554                status = [(NSHTTPURLResponse *)response statusCode];
555            }
556
557            switch (status) {
558                case 0:
559                    newmsg = @"Upload completed without a status code (?!?!?!).";
560                    break;
561                case 200: {
562                    Upload response(static_cast<const char*>([data bytes]),
563                                    static_cast<std::size_t>([data length]));
564                    design = response.mId;
565                    if (design) {
566                        newmsg =  @"Upload completed successfully.";
567                        retry = YES;
568                        mSuccessId = design;
569                    } else {
570                        newmsg = @"The gallery indicates that the upload succeeded but did not return a design number.";
571                    }
572                    break;
573                }
574                case 409:
575                case 401:
576                case 400:
577                case 404:
578                case 500:
579                    retry = YES;
580                default: {
581                    // Take the raw HTML data and then initialize an NSMutableAttributed
582                    // string with HTML code
583                    theParsedHTML = [[NSAttributedString alloc] initWithHTML:data
584                                                          documentAttributes: nil];
585
586                    // Make a copy of retry with __block storage. We don't want to make retry
587                    // a __block variable because this block is going to end up on the
588                    // heap.
589                    __block BOOL stopped = retry;
590                    // This UUID will only be found in the response body if the upload
591                    // failed. Give the user another chance if failure occured.
592                    [data enumerateByteRangesUsingBlock: ^(const void *bytes, NSRange byteRange, BOOL *stop)
593                        {
594                            if (byteRange.length && strnstr(static_cast<const char*>(bytes),
595                                                            "AFD8D2F0-B6EB-4569-9D89-954604507F3B",
596                                                            byteRange.length))
597                            {
598                                stopped = YES;
599                                *stop = YES;
600                            }
601                        }];
602                    retry = stopped;
603                    mSuccessId = 0;
604                    break;
605                }
606            }
607        }
608        dispatch_async(dispatch_get_main_queue(), ^{
609            if (theParsedHTML) {
610                [[mMessage textStorage] setAttributedString:theParsedHTML];
611                [mMessage setBackgroundColor:[NSColor whiteColor]];
612                [theParsedHTML release];
613            } else if (newmsg) {
614                [mMessage setString: newmsg];
615            }
616            [mRetryButton setEnabled: retry];
617            if (retry && design)
618                [mRetryButton setTitle: @"See Design"];
619            else
620                [mRetryButton setTitle: @"Try Again"];
621            [self setView: mDoneView];
622        });
623    }];
624    [mUploadTask retain];
625    [mUploadTask resume];
626
627    [mRetryButton setEnabled:NO];
628    [self setView: mProgressView];
629}
630
631
632- (IBAction)cancel:(id)sender
633{
634    NSWindow* window = [self window];
635    [NSApp endSheet: window];
636    [window orderOut: sender];
637}
638
639- (NSArray<NSString*>*)tokenField:(NSTokenField *)tokenField
640          completionsForSubstring:(NSString *)substring
641                     indexOfToken:(NSInteger)tokenIndex
642              indexOfSelectedItem:(NSInteger *)selectedIndex
643{
644    NSInteger start = -1;
645    for (NSUInteger i = 0; i < [mTags count]; ++i)
646        if ([mTags[i] hasPrefix: substring]) {
647            start = i;
648            break;
649        }
650    if (start == -1)
651        return nil;
652    NSUInteger count = 1;
653    for (NSUInteger i = start + 1; i < [mTags count]; ++i)
654        if ([mTags[i] hasPrefix: substring])
655            ++count;
656        else
657            break;
658    return [mTags subarrayWithRange: NSMakeRange(start, count)];
659}
660
661
662@end
663