// // BloggenAppDelegate.m // Bloggen // // Created by Davis Remmel on 12/2/13. // Copyright (c) 2013 Davis Remmel. All rights reserved. // #import "BloggenAppDelegate.h" #import "SSKeychain/SSKeychain.h" #import "BloggenGenerator.h" #import "BloggenSandboxUtilities.h" #import "BloggenValidator.h" // Default theme values static const int DEFAULT_NUM_FRONTPAGE_POSTS = 3; static const int DEFAULT_NUM_FRONTPAGE_POST_BODIES = 1; static NSString * const DEFAULT_SLUG_FORMAT = @"YYYY/MM/dd"; BOOL OKToGenerate; // Changes when a generator instance is active NSMenu *BloggenMenu; NSString *CurrentProtocol; NSString *PreviousProtocol; NSUserDefaults *Preferences; NSInteger NumberOfFrontpagePosts; NSInteger NumberOfFrontpageFullPostBodies; NSString *SlugFormat; NSString *ThemeNameFallback; // In case theme import doesn't work, we can select this again NSString *CurrentServer; NSString *CurrentUsername; NSString *CurrentPassword; NSString *PreviousServer; NSString *FTPUsername; NSString *FTPPassword; NSString *FTPServer; NSString *S3AccessKeyID; NSString *S3SecretAccessKey; NSString *S3Bucket; NSString *RsyncUsername; NSString *RsyncServer; // These are not stored as security-scoped bookmarks -- if they were, // they couldn't be resolved by builds of this program that differ // from the one that originally-saved them. NSData *LocalBlogBookmark; NSData *LocalThemeBookmark; NSData *SSHDirectoryBookmark; NSData *KeyFileBookmark; NSDictionary *AvailableThemesDictionary; NSFileManager *FileManager; //////////////////////////////////////////////////////////////////////// @implementation BloggenAppDelegate @synthesize passwordTextField; @synthesize usernameTextField; @synthesize serverTextField; @synthesize protocolSegmentedControl; @synthesize statusItem; @synthesize themePopupButton; @synthesize keyButton; @synthesize blogPathButton; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application [self setupMenuBarItem]; [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self]; // Show notifications even when app is active OKToGenerate = YES; // No generator instances are running Preferences = [NSUserDefaults standardUserDefaults]; FileManager = [[NSFileManager alloc] init]; // Global file manager ensures atomic usage [self loadPreferences]; } - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification { return YES; // Show notifications even when app is active } - (void)showNotification:(NSString *)title moreText:(NSString *)text { NSUserNotification *notification = [[NSUserNotification alloc] init]; [notification setTitle:title]; [notification setInformativeText:text]; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } - (void)setupMenuBarItem { BloggenMenu = [[NSMenu alloc] init]; statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; // The text that will be shown in the menu bar [statusItem setTitle:@""]; // The image that will be shown in the menu bar, a 16x16 png works best [statusItem setImage:[NSImage imageNamed:@"menubar-icon"]]; // The highlighted image, use a white version of the normal image [statusItem setAlternateImage:[NSImage imageNamed:@"menubar-icon-hl"]]; // The image gets a blue background when the item is selected [statusItem setHighlightMode:YES]; // Generate initial menu items [BloggenMenu addItemWithTitle:@"Publish Now" action:@selector(startGeneratorAndUpload) keyEquivalent:@""]; [BloggenMenu addItem:[NSMenuItem separatorItem]]; // A thin grey line [BloggenMenu addItemWithTitle:@"Preferences..." action:@selector(openPreferencesWindow) keyEquivalent:@""]; [BloggenMenu addItem:[NSMenuItem separatorItem]]; // A thin grey line [BloggenMenu addItemWithTitle:@"About Bloggen" action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [BloggenMenu addItemWithTitle:@"Bloggen Help" action:@selector(showOnlineHelp) keyEquivalent:@""]; [BloggenMenu addItem:[NSMenuItem separatorItem]]; // A thin grey line [BloggenMenu addItemWithTitle:@"Quit Bloggen" action:@selector(terminate:) keyEquivalent:@""]; [self refreshMenuItems]; } - (void)showOnlineHelp { // Online help is easier to maintain than a dedicated help file [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://apps.davisr.me/bloggen/help"]]; } - (void)refreshMenuItems { [statusItem setMenu:BloggenMenu]; } - (void)startGeneratorAndUpload { BOOL prefsAreOK = [self arePreferencesValidForGeneration]; if (OKToGenerate && prefsAreOK) { [self performSelectorInBackground:@selector(generateNewPosts) withObject:nil]; [self showNotification:@"Bloggen started" moreText:@"Your posts are being processed."]; } else if (!OKToGenerate) { NSLog(@"Can't start a new generator instance; blog is currently generating."); [self showNotification:@"Bloggen in-process" moreText:@"Bloggen has already started generating your blog. Please wait until it finishes."]; } else if (!prefsAreOK) { NSLog(@"Can't start a new generator instance; preferences are invalid."); } } - (void)generateNewPosts { [self savePreferences]; // In case a user runs the generator while the prefs window is still open [self loadPreferences]; // Get save-validated results // Disable the generator menu item; it's a clue to the user not to click it. SEL originalMenuItemAction = [[BloggenMenu itemAtIndex:0] action]; NSString *originalMenuItemTitle = [[BloggenMenu itemAtIndex:0] title]; [[BloggenMenu itemAtIndex:0] setAction:nil]; [[BloggenMenu itemAtIndex:0] setTitle:@"Publishing…"]; [self refreshMenuItems]; OKToGenerate = NO; BOOL serverHasChanged = ![CurrentServer isEqualToString:PreviousServer]; // Start a generator instance BloggenGenerator *blogGenerator = [[BloggenGenerator alloc] init]; int generatorErrorCode = [blogGenerator generateThisBlog:[BloggenSandboxUtilities ssURLForBookmark:LocalBlogBookmark] withTemplate:[BloggenSandboxUtilities ssURLForBookmark:LocalThemeBookmark] withNumberOfFrontpagePosts:NumberOfFrontpagePosts withNumberOfFrontpageFullPostBodies:NumberOfFrontpageFullPostBodies withSlugFormat:SlugFormat uploadWith:CurrentProtocol withUsername:CurrentUsername withPassword:CurrentPassword withSSHBookmark:SSHDirectoryBookmark withKey:[BloggenSandboxUtilities ssURLForBookmark:KeyFileBookmark] toServer:CurrentServer hasServerChanged:serverHasChanged ]; blogGenerator = nil; // ARC releases memory when objects are set to 'nil' // Handle the generator's error code // In the case of an error, PreviousServer is set to an empty string // so that the server gets a clean version of the site when trying // again on the next generation. switch (generatorErrorCode) { case 0: // No error [self showNotification:@"Bloggen complete" moreText:@"Your blog has been updated."]; PreviousServer = CurrentServer; // Update the server strings (these get saved soon) break; case 1: [self showNotification:@"Bloggen failed" moreText:@"Your blog did not generate successfully."]; PreviousServer = @""; break; case 2: [self showNotification:@"Upload failed" moreText:@"An upload error occured. Please check that your upload settings are correct."]; PreviousServer = @""; break; case 3: [self showNotification:@"Couldn't archive pages" moreText:@"There was a problem archiving the generated web pages. Please verify your disk permissions."]; PreviousServer = @""; break; case -1: [self showNotification:@"Bloggen failed" moreText:@"An error occured; check that your preferences are correct."]; PreviousServer = @""; break; default: NSLog(@"Somehow, the generator returned an unknown error code. Solar flare, maybe?"); PreviousServer = @""; break; } OKToGenerate = YES; // Re-enable the menu item [[BloggenMenu itemAtIndex:0] setAction:originalMenuItemAction]; [[BloggenMenu itemAtIndex:0] setTitle:originalMenuItemTitle]; [self refreshMenuItems]; [self savePreferences]; } - (void)openPreferencesWindow { //NSLog(@"Opening preferences.\n"); [self.preferencePanel makeKeyAndOrderFront:self]; [self loadPreferences]; } - (BOOL)arePreferencesValidForGeneration { int errorCode = 0; // AT LEAST one post file must exist NSURL *postsDirectorySSURL = [BloggenSandboxUtilities ssURLForBookmark:LocalBlogBookmark]; if (![[postsDirectorySSURL path] length]) { // The user must have renamed or moved their blog directory. Because // we don't have access to the blog's parent directory, our security- // scoped resources won't work anymore. So, we need to prompt // the user to update their blog path. It's an evil of the app // sandbox. [self changeBlogPathWithOpenPanelMessage:@"You must have moved or renamed your blog. Please select it again, or make a new blog folder."]; postsDirectorySSURL = [BloggenSandboxUtilities ssURLForBookmark:LocalBlogBookmark]; } BOOL postsExist = NO; if ([postsDirectorySSURL startAccessingSecurityScopedResource]) { NSArray *pathContents = [FileManager contentsOfDirectoryAtPath:[postsDirectorySSURL path] error:nil]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(pathExtension == 'md') OR (pathExtension == 'rtfd')"]; postsExist = ([[pathContents filteredArrayUsingPredicate:predicate] count] > 0); [postsDirectorySSURL stopAccessingSecurityScopedResource]; } // The template must exist in Themes directory BOOL templateExists = NO; NSURL *templateSSURL = [BloggenSandboxUtilities ssURLForBookmark:LocalThemeBookmark]; if ([templateSSURL startAccessingSecurityScopedResource]) { templateExists = [FileManager fileExistsAtPath:[templateSSURL path]]; [templateSSURL stopAccessingSecurityScopedResource]; } // If the rsync protocol is selected, there must be .ssh access BOOL usingRsync = [@"Rsync" isEqualToString:CurrentProtocol]; if (postsExist && templateExists) { errorCode = 0; } if (!postsExist && templateExists) { errorCode = 1; } else if (postsExist && !templateExists) { errorCode = 2; } else if (!postsExist && !templateExists) { errorCode = 3; } if (usingRsync) { if (![self authorizeAccessToSSHDirectory]) { errorCode = 4; } } /* Error message handling. So we don't bombard the user with messages, we'll pick the best one. Code | Definition ---------------- 0 | No error. 1 | Posts could not be located. 2 | Templates could not be located. 3 | Posts and templates could not be located. 4 | Rsync needs access to .ssh before continuing. */ switch (errorCode) { case 1: [self showNotification:@"No posts found" moreText:@"Please check the blog directory in Preferences, and make sure a Posts folder exists."]; NSLog(@"No valid post files exist in %@", [postsDirectorySSURL path]); break; case 2: [self showNotification:@"Theme doesn't exist" moreText:@"Please choose a theme in Preferences."]; NSLog(@"The currently selected theme does not exist in %@", [[BloggenSandboxUtilities urlForBookmark:LocalThemeBookmark] path]); break; case 3: [self showNotification:@"No posts or themes" moreText:@"Please specify a blog directory in Preferences. No posts or themes could be located."]; NSLog(@"No posts or themes could be located in the specified blog path."); break; case 4: [self showNotification:@"Authorize rsync" moreText:@"Rsync must be authorized before in order to upload."]; NSLog(@"Rsync was disallowed from using .ssh."); break; default: break; } // Is everything OK? if (postsExist && templateExists) { return YES; } else { return NO; } } - (void)loadPreferences { //NSLog(@"Loading preferences."); [Preferences synchronize]; // Usernames if ([[Preferences stringForKey:@"ftpUsernameString"] length]) { FTPUsername = [Preferences stringForKey:@"ftpUsernameString"]; } if ([[Preferences stringForKey:@"s3AccessKeyIDString"] length]) { S3AccessKeyID = [Preferences stringForKey:@"s3AccessKeyIDString"]; } if ([[Preferences stringForKey:@"rsyncUsernameString"] length]) { RsyncUsername = [Preferences stringForKey:@"rsyncUsernameString"]; } // Passwords NSString *ftpService = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".FTPPassword"]; if ([SSKeychain passwordForService:ftpService account:NSUserName()]) { FTPPassword = [SSKeychain passwordForService:ftpService account:NSUserName()]; } NSString *s3Service = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".S3SecretAccessKey"]; if ([SSKeychain passwordForService:s3Service account:NSUserName()]) { S3SecretAccessKey = [SSKeychain passwordForService:s3Service account:NSUserName()]; } // Servers if ([[Preferences stringForKey:@"ftpServerString"] length]) { FTPServer = [Preferences stringForKey:@"ftpServerString"]; } if ([[Preferences stringForKey:@"s3BucketString"] length]) { S3Bucket = [Preferences stringForKey:@"s3BucketString"]; } if ([[Preferences stringForKey:@"rsyncServerString"] length]) { RsyncServer = [Preferences stringForKey:@"rsyncServerString"]; } if ([[Preferences stringForKey:@"previousServerString"] length]) { PreviousServer = [Preferences stringForKey:@"previousServerString"]; } // Bookmarks if ([Preferences objectForKey:@"sshDirectoryBookmark"]) { SSHDirectoryBookmark = [Preferences objectForKey:@"sshDirectoryBookmark"]; } if ([Preferences objectForKey:@"keyFileBookmark"]) { NSData *possiblyStaleBookmark = [Preferences objectForKey:@"keyFileBookmark"]; NSURL *fileURL = [BloggenSandboxUtilities urlForBookmark:possiblyStaleBookmark]; KeyFileBookmark = [BloggenSandboxUtilities bookmarkForURL:fileURL]; // Remove staleness [[self keyButton] setState:1]; [[self keyButton] setImage:[NSImage imageNamed:@""]]; NSString *keyFileName = [fileURL lastPathComponent]; NSString *keyTitleString = [NSString stringWithFormat:@"\xF0\x9F\x94\x91 %@", keyFileName]; // Hex is emoji "Key" icon [[self keyButton] setTitle:keyTitleString]; } if ([Preferences objectForKey:@"localBlogBookmark"]) { NSData *possiblyStaleBookmark = [Preferences objectForKey:@"localBlogBookmark"]; NSURL *fileURL = [BloggenSandboxUtilities urlForBookmark:possiblyStaleBookmark]; LocalBlogBookmark = [BloggenSandboxUtilities bookmarkForURL:fileURL]; // Remove staleness [blogPathButton setTitle:[fileURL lastPathComponent]]; } if ([Preferences objectForKey:@"localThemeBookmark"]) { NSData *possiblyStaleBookmark = [Preferences objectForKey:@"localThemeBookmark"]; NSURL *fileURL = [BloggenSandboxUtilities urlForBookmark:possiblyStaleBookmark]; LocalThemeBookmark = [BloggenSandboxUtilities bookmarkForURL:fileURL]; // Remove staleness } // Frontpage posts (hidden from UI) NumberOfFrontpagePosts = [Preferences integerForKey:@"numberOfFrontpagePosts"]; NumberOfFrontpageFullPostBodies = [Preferences integerForKey:@"numberOfFrontpageFullPostBodies"]; // Slug format (hidden from UI) SlugFormat = [Preferences stringForKey:@"slugFormatString"]; [self findAndLoadAvailableThemesIntoPopupButton]; // Protocol must be changed last, otherwise other preferences get overwritten with blank values [protocolSegmentedControl setSelectedSegment:[Preferences integerForKey:@"protocolSegmentIndex"]]; [self protocolChanged:[protocolSegmentedControl cell]]; // Updates text fields AND SAVES [self savePreferences]; } - (void)savePreferences { //NSLog(@"Saving preferences."); [Preferences setInteger:[protocolSegmentedControl selectedSegment] forKey:@"protocolSegmentIndex"]; // Usernames [Preferences setObject:FTPUsername forKey:@"ftpUsernameString"]; [Preferences setObject:S3AccessKeyID forKey:@"s3AccessKeyIDString"]; [Preferences setObject:RsyncUsername forKey:@"rsyncUsernameString"]; // Passwords NSString *ftpService = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".FTPPassword"]; if ([FTPPassword length]) { [SSKeychain setPassword:FTPPassword forService:ftpService account:NSUserName()]; } NSString *s3Service = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".S3SecretAccessKey"]; if ([S3SecretAccessKey length]) { [SSKeychain setPassword:S3SecretAccessKey forService:s3Service account:NSUserName()]; } // Servers [Preferences setObject:FTPServer forKey:@"ftpServerString"]; [Preferences setObject:S3Bucket forKey:@"s3BucketString"]; [Preferences setObject:RsyncServer forKey:@"rsyncServerString"]; [Preferences setObject:PreviousServer forKey:@"previousServerString"]; [Preferences setObject:LocalBlogBookmark forKey:@"localBlogBookmark"]; [Preferences setObject:LocalThemeBookmark forKey:@"localThemeBookmark"]; [Preferences setObject:SSHDirectoryBookmark forKey:@"sshDirectoryBookmark"]; [Preferences setObject:KeyFileBookmark forKey:@"keyFileBookmark"]; // Validate the theme variables before saving if (!NumberOfFrontpagePosts) { // Set the program's default value if nothing exists [Preferences setInteger:DEFAULT_NUM_FRONTPAGE_POSTS forKey:@"numberOfFrontpagePosts"]; } else { // Do nothing to the user-changed value } if (!NumberOfFrontpageFullPostBodies) { // Set the program's default value [Preferences setInteger:DEFAULT_NUM_FRONTPAGE_POST_BODIES forKey:@"numberOfFrontpageFullPostBodies"]; } else { // The user changed this value. // Be sure the changed value isn't higher than the numberOfFrontpagePosts if (NumberOfFrontpageFullPostBodies > NumberOfFrontpagePosts) { [Preferences setInteger:DEFAULT_NUM_FRONTPAGE_POSTS forKey:@"numberOfFrontpageFullPostBodies"]; } } // Validate the slug format before saving if (!SlugFormat || [SlugFormat isEqualToString:@""]) { // Set the default since nothing exists [Preferences setObject:DEFAULT_SLUG_FORMAT forKey:@"slugFormatString"]; } else { // Do nothing to the user-changed value } [Preferences synchronize]; } - (void)findAndLoadAvailableThemesIntoPopupButton { // Remove all previous themes [themePopupButton removeAllItems]; [themePopupButton setEnabled:YES]; [themePopupButton setState:1]; // Find themes NSMutableArray *temporaryThemesMutableArray = [[NSMutableArray alloc] init]; NSString *sandboxedDocumentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSURL *sandboxedDocumentsURL = [NSURL fileURLWithPath:sandboxedDocumentDirectory isDirectory:YES]; NSURL *availableThemesURL = [sandboxedDocumentsURL URLByAppendingPathComponent:@"Bloggen Themes" isDirectory:YES]; NSURL *sampleBlogURL = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"Template Blog" isDirectory:YES]; NSURL *sampleThemesURL = [sampleBlogURL URLByAppendingPathComponent:@"Themes" isDirectory:YES]; // If no themes yet exist, copy the themes from the Template Blog in // the main bundle's Supporting Files to the sandboxed themes URL. if (![FileManager fileExistsAtPath:[availableThemesURL path]]) { NSError *error = nil; if (![FileManager copyItemAtURL:sampleThemesURL toURL:availableThemesURL error:&error]) { NSLog(@"Error copying sample themes to sandboxed theme directory: %@", [error localizedDescription]); } } NSArray *tempThemesURLArray = [FileManager contentsOfDirectoryAtURL:availableThemesURL includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; [temporaryThemesMutableArray addObjectsFromArray:tempThemesURLArray]; NSArray *themesURLArray = [temporaryThemesMutableArray copy]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"pathExtension == 'bloggentheme'"]; NSMutableDictionary *availableThemesMutableDictionary = [[NSMutableDictionary alloc] init]; for (NSURL *themeURL in [themesURLArray filteredArrayUsingPredicate:predicate]) { NSString *themeNameString = [[themeURL URLByDeletingPathExtension] lastPathComponent]; [availableThemesMutableDictionary setObject:themeURL forKey:themeNameString]; } AvailableThemesDictionary = [availableThemesMutableDictionary copy]; // List themes if ([availableThemesMutableDictionary count]) { for (NSString *themeName in [[AvailableThemesDictionary allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]) { [themePopupButton addItemWithTitle:themeName]; } // Enable the popup button if it was disabled [themePopupButton setEnabled:YES]; // If the user has a theme saved to preferences, use that if (LocalThemeBookmark) { NSURL *localThemeURL = [BloggenSandboxUtilities urlForBookmark:LocalThemeBookmark]; NSString *themeName = [[localThemeURL URLByDeletingPathExtension] lastPathComponent]; [themePopupButton selectItemWithTitle:themeName]; } else { [themePopupButton selectItemAtIndex:0]; [self setLocalThemeURL]; } ThemeNameFallback = [themePopupButton titleOfSelectedItem]; } else { // No themes detected // This should never be a problem, since if no themes are found, // sample themes are copied from the main app bundle. However, // it may be possible that those sample themes copied incorrectly, // so I'm leaving this here as a vestige, just in case. NSLog(@"No themes could be found in %@", [availableThemesURL path]); [themePopupButton addItemWithTitle:@"No themes available"]; [themePopupButton selectItemAtIndex:0]; } // Finally, add a menu option for loading a new theme from a file [[themePopupButton menu] addItem:[NSMenuItem separatorItem]]; [[themePopupButton menu] addItemWithTitle:@"Import themes…" action:@selector(addThemeToPopupButtonFromFile) keyEquivalent:@""]; } - (void)addThemeToPopupButtonFromFile { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; // Configure panel options [openPanel setCanChooseFiles:YES]; [openPanel setCanChooseDirectories:NO]; [openPanel setAllowsMultipleSelection:YES]; [openPanel setTitle:@"Import Themes"]; [openPanel setMessage:@"Please select any themes you want to import."]; [openPanel setPrompt:@"Import"]; [openPanel setAllowedFileTypes:@[ @"bloggentheme" ]]; if ([openPanel runModal] == NSModalResponseOK) { NSArray *fileURLArray = [openPanel URLs]; for (NSURL *fileURL in fileURLArray) { [self processFile:[fileURL path]]; } } else { [themePopupButton selectItemWithTitle:ThemeNameFallback]; } } - (IBAction)protocolChanged:(NSSegmentedCell *)sender { // The sender is never used in this function, but is required by // Interface Builder. Instead, the text fields are addressed directly, // since this function deals with those ones specifically. [self setAllVariablesFromTextFields:PreviousProtocol]; CurrentProtocol = [protocolSegmentedControl labelForSegment:[protocolSegmentedControl selectedSegment]]; if ([@"FTP" isEqualToString:CurrentProtocol]) { [[[self usernameTextField] cell] setPlaceholderString:@"Username"]; [[[self passwordTextField] cell] setPlaceholderString:@"Password"]; [[[self serverTextField] cell] setPlaceholderString:@"Server (e.g. ftp.yoursite.com/blog)"]; [[self keyButton] setEnabled:NO]; [[self keyButton] setTransparent:YES]; [[self passwordTextField] setEnabled:YES]; [[self passwordTextField] setHidden:NO]; if ([FTPUsername length]) { [[self usernameTextField] setStringValue:FTPUsername]; } if ([FTPPassword length]) { [[self passwordTextField] setStringValue:FTPPassword]; } if ([FTPServer length]) { [[self serverTextField] setStringValue:FTPServer]; } } else if ([@"S3" isEqualToString:CurrentProtocol]) { [[[self usernameTextField] cell] setPlaceholderString:@"Access Key ID"]; [[[self passwordTextField] cell] setPlaceholderString:@"Secret Access Key"]; [[[self serverTextField] cell] setPlaceholderString:@"Bucket name (e.g. blog.yoursite.com)"]; [[self keyButton] setEnabled:NO]; [[self keyButton] setTransparent:YES]; [[self passwordTextField] setEnabled:YES]; [[self passwordTextField] setHidden:NO]; if ([S3AccessKeyID length]) { [[self usernameTextField] setStringValue:S3AccessKeyID]; } if ([S3SecretAccessKey length]) { [[self passwordTextField] setStringValue:S3SecretAccessKey]; } if ([S3Bucket length]) { [[self serverTextField] setStringValue:S3Bucket]; } } else if ([@"Rsync" isEqualToString:CurrentProtocol]) { [[[self usernameTextField] cell] setPlaceholderString:@"Username"]; [[[self passwordTextField] cell] setPlaceholderString:@"Must use key file"]; [[[self serverTextField] cell] setPlaceholderString:@"Server (e.g. yoursite.com:/var/www)"]; [[self keyButton] setEnabled:YES]; [[self keyButton] setTransparent:NO]; [[self passwordTextField] setEnabled:NO]; [[self passwordTextField] setHidden:YES]; if ([RsyncUsername length]) { [[self usernameTextField] setStringValue:RsyncUsername]; } [[self passwordTextField] setStringValue:@""]; if ([RsyncServer length]) { [[self serverTextField] setStringValue:RsyncServer]; } } //NSLog(@"Protocol changed to %@", currentProtocol); PreviousProtocol = CurrentProtocol; [self setAllVariablesFromTextFields:CurrentProtocol]; [self savePreferences]; } - (IBAction)changeBlogPathButtonPressed:(NSButton *)sender { [self changeBlogPathWithOpenPanelMessage:@"Please make a new folder for your blog, or select an existing one containing posts."]; } - (void)changeBlogPathWithOpenPanelMessage:(NSString *)overrideMessage { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; // Configure panel options [openPanel setCanChooseFiles:NO]; [openPanel setCanChooseDirectories:YES]; [openPanel setCanCreateDirectories:YES]; [openPanel setAllowsMultipleSelection:NO]; [openPanel setTitle:@"Open Blog"]; [openPanel setPrompt:@"Use Blog"]; if ([overrideMessage length]) { [openPanel setMessage:overrideMessage]; } if ([openPanel runModal] == NSModalResponseOK) { LocalBlogBookmark = [BloggenSandboxUtilities bookmarkForURL:[openPanel URL]]; // If the user created a new folder for their blog, it will probably be // empty. If an empty folder is found, Bloggen should create the Posts // and Themes directories for users. A template directory is included // in the Supporting Files, that includes a sample post and themes // to get the user started. NSURL *openedSSURL = [BloggenSandboxUtilities ssURLForBookmark:LocalBlogBookmark]; if ([openedSSURL startAccessingSecurityScopedResource]) { NSArray *existingFiles = [FileManager contentsOfDirectoryAtURL:openedSSURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; if (0 == [existingFiles count]) { // Copy over sample posts NSURL *sampleBlogURL = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"Template Blog" isDirectory:YES]; NSURL *samplePostsURL = [sampleBlogURL URLByAppendingPathComponent:@"Posts" isDirectory:YES]; // First, we need to make an array of the files contained // within our Sample Posts directory. NSArray *samplePostsArray = [FileManager contentsOfDirectoryAtURL:samplePostsURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; for (NSURL *url in samplePostsArray) { NSError *copyError = nil; if (![FileManager copyItemAtURL:url toURL:[openedSSURL URLByAppendingPathComponent:[url lastPathComponent]] error:©Error]) { NSLog(@"Couldn't copy sample post to new folder: %@", [copyError localizedDescription]); } } } [openedSSURL stopAccessingSecurityScopedResource]; } } [self savePreferences]; [self loadPreferences]; } - (IBAction)themePopupButtonChanged:(NSPopUpButton *)sender { [self setLocalThemeURL]; } - (void)setLocalThemeURL { NSURL *themeURL = [AvailableThemesDictionary objectForKey:[themePopupButton titleOfSelectedItem]]; LocalThemeBookmark = [BloggenSandboxUtilities bookmarkForURL:themeURL]; [self savePreferences]; } - (void)setAllVariablesFromTextFields:(NSString *)protocol { if ([@"FTP" isEqualToString:protocol]) { FTPUsername = [usernameTextField stringValue]; FTPPassword = [passwordTextField stringValue]; FTPServer = [serverTextField stringValue]; CurrentUsername = FTPUsername; CurrentPassword = FTPPassword; CurrentServer = FTPServer; } else if ([@"S3" isEqualToString:protocol]) { S3AccessKeyID = [usernameTextField stringValue]; S3SecretAccessKey = [passwordTextField stringValue]; S3Bucket = [serverTextField stringValue]; CurrentUsername = S3AccessKeyID; CurrentPassword = S3SecretAccessKey; CurrentServer = S3Bucket; } else if ([@"Rsync" isEqualToString:protocol]) { RsyncUsername = [usernameTextField stringValue]; RsyncServer = [serverTextField stringValue]; CurrentUsername = RsyncUsername; CurrentPassword = @""; CurrentServer = RsyncServer; } [self savePreferences]; } - (IBAction)usernameTextFieldChanged:(NSTextField *)sender { [self setAllVariablesFromTextFields:CurrentProtocol]; [self savePreferences]; } - (IBAction)passwordTextFieldChanged:(NSSecureTextField *)sender { if (![[passwordTextField stringValue] length]) { [[self keyButton] setState:0]; // Not using a key file, then [[self keyButton] setImage:[NSImage imageNamed:@"key-disabled"]]; } [self setAllVariablesFromTextFields:CurrentProtocol]; [self savePreferences]; } - (IBAction)serverTextFieldChanged:(NSTextField *)sender { [self setAllVariablesFromTextFields:CurrentProtocol]; [self savePreferences]; } - (IBAction)keyButtonPressed:(NSButton *)sender { if (1 == [sender state]) { if ([self authorizeAccessToSSHDirectory]) { // Open a key file NSOpenPanel *openPanel = [NSOpenPanel openPanel]; // Configure panel options [openPanel setCanChooseFiles:YES]; [openPanel setCanChooseDirectories:NO]; [openPanel setAllowsMultipleSelection:NO]; [openPanel setTitle:@"Open Identity"]; [openPanel setMessage:@"Please select an SSH key file. Keys added with ssh-agent are used automatically."]; [openPanel setPrompt:@"Use Key File"]; [openPanel setAllowedFileTypes:@[ @"", @"key", @"pem" ]]; if ([openPanel runModal] == NSModalResponseOK) { KeyFileBookmark = [BloggenSandboxUtilities bookmarkForURL:[openPanel URL]]; [self savePreferences]; [[self keyButton] setImage:[NSImage imageNamed:@""]]; NSString *keyFileName = [[BloggenSandboxUtilities ssURLForBookmark:KeyFileBookmark] lastPathComponent]; NSString *keyTitleString = [NSString stringWithFormat:@"\xF0\x9F\x94\x91 %@", keyFileName]; // Hex is KEY emoji [[self keyButton] setTitle:keyTitleString]; } else { [[self keyButton] setState:0]; // Keep the button off } } } else { // No key file KeyFileBookmark = nil; [self savePreferences]; [[self keyButton] setTitle:@""]; [[self keyButton] setImage:[NSImage imageNamed:@"key-disabled"]]; } } - (BOOL)authorizeAccessToSSHDirectory { // Because of sandbox restrictions, the UploadHelper XPC service cannot // access ~/.ssh without user-granted consent via the powerbox. Therefore, // we need to ask the user to locate their ~/.ssh directory, and grant // access for Bloggen to create a bookmark to pass onto the XPC service. BOOL haveSSHDirectory = (nil != SSHDirectoryBookmark); BOOL retry = NO; NSURL *selectedURL = nil; while (!haveSSHDirectory) { NSOpenPanel *sshOpenPanel = [NSOpenPanel openPanel]; if (!retry) { [sshOpenPanel setMessage:@"Before Bloggen can use rsync, it must be authorized to use the .ssh folder containing your SSH config file.\nPlease select it (in your Home folder), then click Authorize."]; } else { [sshOpenPanel setMessage:@"That folder didn't contain an SSH config file.\nPlease select the the .ssh folder containing your SSH config file, then click Authorize."]; } [sshOpenPanel setShowsHiddenFiles:YES]; [sshOpenPanel setCanChooseFiles:NO]; [sshOpenPanel setCanChooseDirectories:YES]; [sshOpenPanel setAllowsMultipleSelection:NO]; [sshOpenPanel setTitle:@"Authorize SSH"]; [sshOpenPanel setPrompt:@"Authorize"]; if ([sshOpenPanel runModal] == NSModalResponseOK) { // SSH requires the .ssh to contain a config file, so that // will be our basis for verification. selectedURL = [sshOpenPanel URL]; NSURL *ssURL = [BloggenSandboxUtilities ssURLForURL:selectedURL]; if ([ssURL startAccessingSecurityScopedResource]) { // Does it contain a config file? NSString *configPath = [[ssURL path] stringByAppendingPathComponent:@"config"]; haveSSHDirectory = [FileManager fileExistsAtPath:configPath]; [ssURL stopAccessingSecurityScopedResource]; if (haveSSHDirectory) { SSHDirectoryBookmark = [BloggenSandboxUtilities bookmarkForURL:selectedURL]; [self savePreferences]; } } retry = YES; } else { // User cancelled the authorization open panel [keyButton setState:0]; return NO; } } return YES; } // Theme (file) Processing // ref: http://stackoverflow.com/questions/5331774/cocoa-obj-c-open-file-when-dragging-it-to-application-icon - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { return [self processFile:filename]; } - (BOOL)processFile:(NSString *)filePath { // The user opened a supported file, as defined in Info.plist. // For now, Bloggen can only open .bloggentheme files. // Returns YES when file is successfully processed. if ([[filePath pathExtension] isEqualToString:@"bloggentheme"]) { NSURL *themeURL = [NSURL fileURLWithPath:filePath isDirectory:YES]; if ([BloggenValidator validateTheme:themeURL]) { // The theme is valid, so copy it to the sandboxed themes // directory. NSString *sandboxedDocumentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSURL *sandboxedDocumentsURL = [NSURL fileURLWithPath:sandboxedDocumentDirectory isDirectory:YES]; NSURL *availableThemesURL = [sandboxedDocumentsURL URLByAppendingPathComponent:@"Bloggen Themes" isDirectory:YES]; NSURL *newThemeURL = [availableThemesURL URLByAppendingPathComponent:[themeURL lastPathComponent] isDirectory:YES]; NSError *error = nil; // If the theme already exists, remove it if ([FileManager fileExistsAtPath:[newThemeURL path]]) { [FileManager removeItemAtURL:newThemeURL error:nil]; } if (![FileManager copyItemAtURL:themeURL toURL:newThemeURL error:&error]) { [self showNotification:@"Couldn't import theme" moreText:@"There was a problem importing the theme. Please verify your permissions with Disk Utility."]; NSLog(@"Error copying theme into sandboxed themes directory: %@", [error localizedDescription]); [themePopupButton selectItemWithTitle:ThemeNameFallback]; } else { // Good; make this theme the current one NSString *newThemeTitle = [[newThemeURL lastPathComponent] stringByDeletingPathExtension]; [self findAndLoadAvailableThemesIntoPopupButton]; [themePopupButton selectItemWithTitle:newThemeTitle]; [self setLocalThemeURL]; return YES; } } else { [self showNotification:@"Bad theme" moreText:@"Sorry, but that theme won't work with Bloggen."]; NSLog(@"User-opened theme is not valid."); [themePopupButton selectItemWithTitle:ThemeNameFallback]; } } return NO; } @end