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