diff options
| -rw-r--r-- | Factories/QSBookmarkProviderFactory.h (renamed from QSBookmarkProviderFactory.h) | 0 | ||||
| -rw-r--r-- | Factories/QSBookmarkProviderFactory.m (renamed from QSBookmarkProviderFactory.m) | 0 | ||||
| -rw-r--r-- | Protocols/QSBookmarkProvider.h (renamed from QSBookmarkProvider.h) | 4 | ||||
| -rw-r--r-- | Providers/QSDeliciousAPIProvider.h (renamed from QSDeliciousAPIProvider.h) | 0 | ||||
| -rw-r--r-- | Providers/QSDeliciousAPIProvider.m (renamed from QSDeliciousAPIProvider.m) | 7 | ||||
| -rw-r--r-- | Providers/QSLinkdingProvider.h (renamed from QSLinkdingProvider.h) | 0 | ||||
| -rw-r--r-- | Providers/QSLinkdingProvider.m (renamed from QSLinkdingProvider.m) | 11 | ||||
| -rw-r--r-- | QSDeliciousPlugIn.xcodeproj/project.pbxproj | 52 | ||||
| -rw-r--r-- | QSDeliciousPlugInTests.m | 89 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.h | 1 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.m | 315 | ||||
| -rw-r--r-- | QSDeliciousPlugIn_Source.xib | 2 | ||||
| -rw-r--r-- | REFACTOR_GUIDE.md | 118 | ||||
| -rw-r--r-- | Types/SocialSite.h (renamed from SocialSite.h) | 0 | ||||
| -rw-r--r-- | Types/SocialSite.m (renamed from SocialSite.m) | 0 |
15 files changed, 221 insertions, 378 deletions
diff --git a/QSBookmarkProviderFactory.h b/Factories/QSBookmarkProviderFactory.h index e2e694b..e2e694b 100644 --- a/QSBookmarkProviderFactory.h +++ b/Factories/QSBookmarkProviderFactory.h diff --git a/QSBookmarkProviderFactory.m b/Factories/QSBookmarkProviderFactory.m index 6fcfdfb..6fcfdfb 100644 --- a/QSBookmarkProviderFactory.m +++ b/Factories/QSBookmarkProviderFactory.m diff --git a/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h index 735ae82..136ea2d 100644 --- a/QSBookmarkProvider.h +++ b/Protocols/QSBookmarkProvider.h @@ -22,7 +22,7 @@ * Fetch bookmarks for the given configuration * Returns an NSArray of QSObject instances */ -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags; +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags; /** * Get the supported site for this provider @@ -45,4 +45,4 @@ */ - (NSString *)tagURLType; -@end
\ No newline at end of file +@end diff --git a/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h index 0064264..0064264 100644 --- a/QSDeliciousAPIProvider.h +++ b/Providers/QSDeliciousAPIProvider.h diff --git a/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m index d97d1d2..ab09f88 100644 --- a/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -70,7 +70,7 @@ return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; } -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { // Try cached data first NSData *data = [self cachedBookmarkDataForSite:site username:username]; @@ -131,7 +131,10 @@ if (tag.length > 0) { QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]]; [tagObject setObject:tag forType:[self tagURLType]]; - [tagObject setObject:username forMeta:[NSString stringWithFormat:@"%@.username", [SocialSiteHelper reversedSiteURLForSite:site]]]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + [tagObject setObject:identifier forMeta:@"source.identifier"]; [tagObject setName:tag]; [tagObject setPrimaryType:[self tagURLType]]; [objects addObject:tagObject]; diff --git a/QSLinkdingProvider.h b/Providers/QSLinkdingProvider.h index 28807e8..28807e8 100644 --- a/QSLinkdingProvider.h +++ b/Providers/QSLinkdingProvider.h diff --git a/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m index f97f5c7..094f318 100644 --- a/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -42,7 +42,7 @@ [data writeToFile:cachePath atomically:NO]; } -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { if (![self canHandleSite:site username:username password:password host:host]) { return @[]; @@ -70,8 +70,6 @@ return @[]; } - NSLog(@"WE ARE ABOUT TO REQUEST TO URL: %@", requestURL); - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; @@ -132,8 +130,11 @@ if (tag.length > 0) { QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; [tagObject setObject:tag forType:[self tagURLType]]; - [tagObject setObject:username forMeta:@"linkding.username"]; - [tagObject setObject:host forMeta:@"linkding.host"]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + // We need the identifier to be able to fetch the keychain password + [tagObject setObject:identifier forMeta:@"source.identifier"]; [tagObject setName:tag]; [tagObject setPrimaryType:[self tagURLType]]; [objects addObject:tagObject]; diff --git a/QSDeliciousPlugIn.xcodeproj/project.pbxproj b/QSDeliciousPlugIn.xcodeproj/project.pbxproj index ebe2b52..d68169d 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -42,8 +42,6 @@ B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSLinkdingProvider.m; sourceTree = "<group>"; }; B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProviderFactory.h; sourceTree = "<group>"; }; B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSBookmarkProviderFactory.m; sourceTree = "<group>"; }; - B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugInTests.m; sourceTree = "<group>"; }; - B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTOR_GUIDE.md; sourceTree = "<group>"; }; B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QSDeliciousPlugIn_Source.xib; sourceTree = "<group>"; }; D475F9AF18B2992D0012243C /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = "<group>"; }; D475F9B018B2992D0012243C /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; @@ -129,23 +127,53 @@ 32DBCF9F0370C38200C91783 /* Other Sources */ = { isa = PBXGroup; children = ( - B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, - B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, - B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + B59368062E6EC67D00DBD0F1 /* Factories */, + B59368052E6EC66500DBD0F1 /* Protocols */, + B59368042E6EC65000DBD0F1 /* Types */, + B59368032E6EC63700DBD0F1 /* Providers */, + B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */, + E182BE3A06FC9B13007BF2C2 /* Localizable.strings */, + E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */, + E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */, + ); + name = "Other Sources"; + sourceTree = "<group>"; + }; + B59368032E6EC63700DBD0F1 /* Providers */ = { + isa = PBXGroup; + children = ( B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */, B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */, B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */, B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */, + ); + path = Providers; + sourceTree = "<group>"; + }; + B59368042E6EC65000DBD0F1 /* Types */ = { + isa = PBXGroup; + children = ( + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, + ); + path = Types; + sourceTree = "<group>"; + }; + B59368052E6EC66500DBD0F1 /* Protocols */ = { + isa = PBXGroup; + children = ( + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + ); + path = Protocols; + sourceTree = "<group>"; + }; + B59368062E6EC67D00DBD0F1 /* Factories */ = { + isa = PBXGroup; + children = ( B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */, B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */, - B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */, - B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */, - B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */, - E182BE3A06FC9B13007BF2C2 /* Localizable.strings */, - E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */, - E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */, ); - name = "Other Sources"; + path = Factories; sourceTree = "<group>"; }; D475F9AE18B2992D0012243C /* Configuration */ = { diff --git a/QSDeliciousPlugInTests.m b/QSDeliciousPlugInTests.m deleted file mode 100644 index 3aff4ff..0000000 --- a/QSDeliciousPlugInTests.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// QSDeliciousPlugInTests.m -// QSDeliciousPlugIn -// - -#import <Testing/Testing.h> -#import "QSBookmarkProviderFactory.h" -#import "QSDeliciousAPIProvider.h" -#import "QSLinkdingProvider.h" -#import "SocialSite.h" - -@suite("QSDeliciousPlugIn Strategy Pattern Tests") -struct QSDeliciousPlugInTests { - - @Test("Factory returns correct provider for Delicious") - func testDeliciousProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id<QSBookmarkProvider> provider = [factory providerForSite:SocialSiteDelicious - username:@"testuser" - password:@"testpass" - host:nil]; - - #expect(provider != nil, "Should return a provider for Delicious"); - #expect([provider isKindOfClass:[QSDeliciousAPIProvider class]], "Should return a QSDeliciousAPIProvider for Delicious"); - #expect([provider supportedSite] == SocialSiteDelicious, "Provider should support Delicious site"); - } - - @Test("Factory returns correct provider for Pinboard") - func testPinboardProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id<QSBookmarkProvider> provider = [factory providerForSite:SocialSitePinboard - username:@"testuser" - password:@"testpass" - host:nil]; - - #expect(provider != nil, "Should return a provider for Pinboard"); - #expect([provider isKindOfClass:[QSDeliciousAPIProvider class]], "Should return a QSDeliciousAPIProvider for Pinboard"); - #expect([provider supportedSite] == SocialSitePinboard, "Provider should support Pinboard site"); - } - - @Test("Factory returns correct provider for Linkding") - func testLinkdingProvider() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - id<QSBookmarkProvider> provider = [factory providerForSite:SocialSiteLinkding - username:@"testuser" - password:@"testtoken" - host:@"https://bookmarks.example.com"]; - - #expect(provider != nil, "Should return a provider for Linkding"); - #expect([provider isKindOfClass:[QSLinkdingProvider class]], "Should return a QSLinkdingProvider for Linkding"); - #expect([provider supportedSite] == SocialSiteLinkding, "Provider should support Linkding site"); - } - - @Test("Factory returns nil for invalid configuration") - func testInvalidConfiguration() async throws { - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - - // Test with empty username - id<QSBookmarkProvider> provider = [factory providerForSite:SocialSiteDelicious - username:@"" - password:@"testpass" - host:nil]; - - #expect(provider == nil, "Should return nil for empty username"); - - // Test Linkding without host - provider = [factory providerForSite:SocialSiteLinkding - username:@"testuser" - password:@"testtoken" - host:@""]; - - #expect(provider == nil, "Should return nil for Linkding without host"); - } - - @Test("SocialSite helper methods work correctly") - func testSocialSiteHelpers() async throws { - #expect([[SocialSiteHelper displayNameForSite:SocialSiteDelicious] isEqualToString:@"del.icio.us"]); - #expect([[SocialSiteHelper displayNameForSite:SocialSiteLinkding] isEqualToString:@"Linkding"]); - - #expect([[SocialSiteHelper siteURLForSite:SocialSitePinboard] isEqualToString:@"pinboard.in"]); - #expect([[SocialSiteHelper siteURLForSite:SocialSiteLinkding] isEqualToString:@""]); - - #expect([[SocialSiteHelper reversedSiteURLForSite:SocialSiteDelicious] isEqualToString:@"us.icio.del"]); - #expect([[SocialSiteHelper reversedSiteURLForSite:SocialSiteLinkding] isEqualToString:@"linkding"]); - } -} diff --git a/QSDeliciousPlugIn_Source.h b/QSDeliciousPlugIn_Source.h index 1eff1c7..56456c5 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -17,6 +17,7 @@ IBOutlet NSTextField *passField; IBOutlet NSTextField *hostField; } +@property (nonatomic, strong) NSString *internalPassword; - (IBAction)settingsChanged:(id)sender; @end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index fb09a97..55eedf0 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -9,9 +9,21 @@ #import "QSDeliciousPlugIn_Source.h" #import <QSCore/QSCore.h> #import <Security/Security.h> +#import <os/log.h> @implementation QSDeliciousPlugIn_Source +#pragma mark - Lifecycle + +// This method will get called whenever we change which +// active entry is selected. +- (void)setSelectedEntry:(id)selectedEntry { + [super setSelectedEntry:selectedEntry]; + [self loadPasswordFromKeychain]; +} + +#pragma mark - Quicksilver Source Methods + - (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } @@ -48,7 +60,7 @@ } - (NSString *)currentPassword { - return [self.selectedEntry.sourceSettings objectForKey:@"password"]; + return self.internalPassword; } - (BOOL)includeTags { @@ -56,174 +68,159 @@ } +// This method is called on action from all the NIB methods +// to force the catalog to save the current values. - (IBAction)settingsChanged:(id)sender { [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:self.selectedEntry]; } -#pragma mark - Keychain Access -- (SecProtocolType)protocolTypeForString:(NSString *)protocol { - if ([protocol isEqualToString:@"ftp"]) return kSecProtocolTypeFTP; - else if ([protocol isEqualToString:@"http"]) return kSecProtocolTypeHTTP; - else if ([protocol isEqualToString:@"sftp"]) return kSecProtocolTypeFTPS; - else if ([protocol isEqualToString:@"eppc"]) return kSecProtocolTypeEPPC; - else if ([protocol isEqualToString:@"afp"]) return kSecProtocolTypeAFP; - else if ([protocol isEqualToString:@"smb"]) return kSecProtocolTypeSMB; - else if ([protocol isEqualToString:@"ssh"]) return kSecProtocolTypeSSH; - else if ([protocol isEqualToString:@"telnet"]) return kSecProtocolTypeTelnet; - return 0; +#pragma mark - Keychain Helper Methods + +- (NSString *)keychainKeyForIdentifier:(NSString *)identifier { + return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; } -- (NSString *)passwordForHost:(NSString *)host user:(NSString *)user andType:(SecProtocolType)type { - const char *buffer; - UInt32 length = 0; - OSErr err; - - err = SecKeychainFindInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, - NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - &length, (void**)&buffer, - NULL); - - if (err == noErr) { - NSString *password = [NSString stringWithUTF8String:buffer]; - SecKeychainItemFreeContent(NULL,(void *)buffer); - return password; - } - return nil; +- (NSString *)passwordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + + UInt32 passwordLength = 0; + void *passwordData = NULL; + + OSStatus status = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + &passwordLength, &passwordData, + NULL); + + if (status == errSecSuccess && passwordData != NULL) { + NSString *password = [[NSString alloc] initWithBytes:passwordData + length:passwordLength + encoding:NSUTF8StringEncoding]; + SecKeychainItemFreeContent(NULL, passwordData); + return password; + } + + return nil; } -- (NSString *)passwordForHost:(NSString *)host user:(NSString *)user andScheme:(NSString *)scheme { - NSString *password = nil; - - SecProtocolType type = [self protocolTypeForString:scheme]; - - password = [self passwordForHost:host user:user andType:type]; - - if (!password && type == kSecProtocolTypeFTP) - password = [self passwordForHost:host user:user andType:kSecProtocolTypeFTPAccount]; // Workaround for Transmit's old type usage - if ( !password ) - password = [self passwordForHost:host user:user andType:0]; - if ( !password ) - NSLog(@"Couldn't find password. URL:%@ %@ %@", host, user,scheme ); - return password; +- (OSStatus)savePasswordToKeychainForKey:(NSString *)key password:(NSString *)password { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + const char *passwordCString = [password UTF8String]; + + // First try to find existing item + SecKeychainItemRef item = NULL; + OSStatus findStatus = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + NULL, NULL, + &item); + + OSStatus status; + if (findStatus == errSecSuccess) { + // Update existing item + status = SecKeychainItemModifyAttributesAndData(item, + NULL, + (UInt32)strlen(passwordCString), + passwordCString); + CFRelease(item); + } else { + // Create new item + status = SecKeychainAddGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + (UInt32)strlen(passwordCString), passwordCString, + NULL); + } + + return status; } -- (NSString *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; +- (OSStatus)deletePasswordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; + + SecKeychainItemRef item = NULL; + OSStatus findStatus = SecKeychainFindGenericPassword(NULL, + (UInt32)strlen(service), service, + (UInt32)strlen(account), account, + NULL, NULL, + &item); + + if (findStatus == errSecSuccess) { + OSStatus deleteStatus = SecKeychainItemDelete(item); + CFRelease(item); + return deleteStatus; + } + + return findStatus; } -- (OSErr)addURLPasswordToKeychain:(NSURL *)url { - OSErr err; - - NSString *host = [url host]; - NSString *user = [url user]; - NSString *pass = [url password]; - - SecProtocolType type = [self protocolTypeForString:[url scheme]]; - - SecKeychainItemRef existing = NULL; - - err = SecKeychainFindInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - NULL,NULL, - &existing); - - if ( !err ) { - err = SecKeychainItemModifyContent( existing, NULL, (UInt32)[pass length], [pass UTF8String] ); - CFRelease( existing ); - } else { - err = SecKeychainAddInternetPassword(NULL, - (UInt32)[host length], [host UTF8String], - 0, NULL, - (UInt32)[user length], [user UTF8String], - 0, NULL, - 0, - type, - 0, - (UInt32)[pass length], [pass UTF8String], - NULL); +#pragma mark - Password Keychain Methods + +- (void)loadPasswordFromKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier) { + self.internalPassword = nil; + return; } - - return err; + + NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + [self setPassword: [self passwordFromKeychainForKey:keychainKey]]; } -- (NSString *)oldCurrentPassword { - NSString *account = [self currentUsername]; - if (!account) return nil; - - SocialSite site = [self siteIndex]; - NSString *host = nil; +- (void)savePasswordToKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier || !self.internalPassword) { + return; + } - // For Linkding, use the custom host; for others, use the standard site URL - if (site == SocialSiteLinkding) { - host = [self currentHost]; - if (!host) return nil; - } else { - host = [SocialSiteHelper siteURLForSite:site]; + NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + OSStatus status = [self savePasswordToKeychainForKey:keychainKey password:self.internalPassword]; + + if (status != errSecSuccess) { + NSLog(@"Failed to save password to keychain for key: %@, status: %d", keychainKey, (int)status); } - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, host]]; - NSString *password = [self keychainPasswordForURL:keychainURL]; - - return password; } -- (void)setCurrentPassword:(NSString *)newPassword { - NSString *account = [self currentUsername]; - if (!account) return; - if ([newPassword length] <= 0) return; - - SocialSite site = [self siteIndex]; - NSString *host = nil; +#pragma mark - Password Property Accessors + +- (NSString *)password { + return self.internalPassword; +} + +- (void)setPassword:(NSString *)password { + self.internalPassword = password; - // For Linkding, use the custom host; for others, use the standard site URL - if (site == SocialSiteLinkding) { - host = [self currentHost]; - if (!host) return; - } else { - host = [SocialSiteHelper siteURLForSite:site]; - } - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@%@/", account, newPassword, host]]; - - [self addURLPasswordToKeychain:keychainURL]; + // Save to keychain asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self savePasswordToKeychain]; + }); } #pragma mark - Objects For Entry -- (NSArray *)objectsForEntry:(NSDictionary *)theEntry { - NSLog(@"WE HAVE BEEN REQUESTED"); +- (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { - SocialSite site = [self siteIndex]; - NSString *username = [self currentUsername]; - NSString *password = [self currentPassword]; - NSString *host = [self currentHost]; - BOOL includeTags = [self includeTags]; + NSDictionary *settings = theEntry.sourceSettings; + + SocialSite site = [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : SocialSiteDelicious; + NSString *username = [settings objectForKey:@"username"]; + NSString *identifier = theEntry.identifier; + NSString *keychainKey = [self keychainKeyForIdentifier:identifier]; + NSString *password = [self passwordFromKeychainForKey:keychainKey]; + NSString *host = [settings objectForKey:@"host"]; + BOOL includeTags = [settings objectForKey:@"includeTags"]; - // Get the appropriate provider using the factory QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; id<QSBookmarkProvider> provider = [factory providerForSite:site username:username password:password host:host]; - NSLog(@"Checking for %ld, user %@, pass %@, host %@", (long)site, username, password, host); if (!provider) { NSLog(@"No provider available for site %ld with username %@", (long)site, username); return @[]; } - return [provider fetchBookmarksForSite:site username:username password:password host:host includeTags:includeTags]; + return [provider fetchBookmarksForSite:site username:username password:password identifier:identifier host:host includeTags:includeTags]; } - (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { @@ -254,10 +251,37 @@ [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; } +// This will receive a tag object. Tag objects will have the +// source configuration in the meta: source.username, +// source.site, source.host and source.identifier. - (BOOL)loadChildrenForObject:(QSObject *)object { - SocialSite site = [self siteIndex]; - - NSString *tagType = nil; + + NSNumber *siteNumber = [object objectForMeta:@"source.site"]; + if (!siteNumber) { + NSLog(@"The tag didn't have a valid site."); + return NO; + } + + SocialSite site = [siteNumber integerValue]; + + NSString *username = [object objectForMeta:@"source.username"]; + if (!username) { + NSLog(@"The tag didn't have a valid username."); + return NO; + } + NSString *identifier = [object objectForMeta:@"source.identifier"]; + if (!identifier) { + NSLog(@"The tag didn't have a valid identifier."); + return NO; + } + + NSString *host = [object objectForMeta:@"source.host"]; + if (site == SocialSiteLinkding && !host) { + NSLog(@"The tag didn't have a host, and its site requires it."); + return NO; + } + + NSString *tagType = nil; if (site == SocialSiteLinkding) { tagType = @"tag.linkding"; } else { @@ -265,18 +289,11 @@ tagType = [NSString stringWithFormat:@"tag.%@", reversedURL]; } - NSString *tag = [object objectForType:tagType]; - if (!tag) return NO; - - NSString *username = nil; - if (site == SocialSiteLinkding) { - username = [object objectForMeta:@"linkding.username"]; - } else { - NSString *reversedURL = [SocialSiteHelper reversedSiteURLForSite:site]; - username = [object objectForMeta:[NSString stringWithFormat:@"%@.username", reversedURL]]; - } - - if (!username) return NO; + NSString *tag = [object objectForType:tagType]; + if (!tag) { + NSLog(@"We could not find a valid tag type."); + return NO; + } NSArray *children = [self objectsForTag:tag username:username]; [object setChildren:children]; diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib index 83b620f..c688346 100644 --- a/QSDeliciousPlugIn_Source.xib +++ b/QSDeliciousPlugIn_Source.xib @@ -144,7 +144,7 @@ </secureTextFieldCell> <connections> <action selector="settingsChanged:" target="-2" id="mWc-uc-BZi"/> - <binding destination="-2" name="value" keyPath="selectedEntry.sourceSettings.password" id="iVD-TC-GTV"/> + <binding destination="-2" name="value" keyPath="password" id="lx1-bz-oz5"/> </connections> </secureTextField> <textField focusRingType="none" verticalHuggingPriority="750" id="host-text-field"> diff --git a/REFACTOR_GUIDE.md b/REFACTOR_GUIDE.md deleted file mode 100644 index 2176a98..0000000 --- a/REFACTOR_GUIDE.md +++ /dev/null @@ -1,118 +0,0 @@ -# QSDeliciousPlugIn Refactor Guide - -This document outlines the refactoring changes made to support multiple social bookmark providers using the Strategy Pattern. - -## Overview - -The plugin has been refactored from a monolithic implementation to a modular, extensible architecture that makes it easy to add new bookmark providers. - -## Architecture - -### Core Components - -1. **SocialSite Enum** (`SocialSite.h/.m`) - - Defines supported bookmark services - - Helper methods for display names and URLs - -2. **QSBookmarkProvider Protocol** (`QSBookmarkProvider.h`) - - Defines the interface all providers must implement - - Key methods: `canHandleSite:username:password:host:`, `fetchBookmarksForSite:username:password:host:includeTags:` - -3. **QSBookmarkProviderFactory** (`QSBookmarkProviderFactory.h/.m`) - - Singleton factory that manages all providers - - Returns the appropriate provider for a given configuration - -4. **Provider Implementations** - - `QSDeliciousAPIProvider`: Handles XML-based Delicious / Pinboard v1 API (Delicious, Magnolia, Pinboard) - - `QSLinkdingProvider`: Handles JSON-based Linkding API - -### Strategy Pattern Implementation - -The main source file now uses the strategy pattern: - -```objective-c -// Get the appropriate provider using the factory -QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; -id<QSBookmarkProvider> provider = [factory providerForSite:site username:username password:password host:host]; - -if (!provider) { - NSLog(@"No provider available for site %ld with username %@", (long)site, username); - return @[]; -} - -return [provider fetchBookmarksForSite:site username:username password:password host:host includeTags:includeTags]; -``` - -## Supported Services - -| Service | ID | API Type | Authentication | Host Required | -|---------|----|---------| -------------- | ------------- | -| del.icio.us | 0 | XML/Basic Auth | Username/Password | No | -| ma.gnolia.com | 1 | XML/Basic Auth | Username/Password | No | -| Pinboard | 2 | XML/Basic Auth | Username/Password | No | -| Linkding | 3 | JSON/Token Auth | Username/API Token | Yes | - -## Adding New Providers - -1. Add a new case to the `SocialSite` enum -2. Update `SocialSiteHelper` methods -3. Create a new provider class implementing `QSBookmarkProvider` -4. Add the provider to `QSBookmarkProviderFactory.setupProviders` - -Example new provider structure: - -```objective-c -@interface QSMyNewProvider : NSObject <QSBookmarkProvider> -@end - -@implementation QSMyNewProvider - -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == SocialSiteMyNew) && username.length > 0 && password.length > 0; -} - -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { - // Implementation here -} - -// ... other required methods -@end -``` - -## Interface Bindings - -The NIB file should be updated to include: - -- **Settings Dictionary** with keys: - - `username` (NSString) - - `password` (NSString) - bound to File's Owner directly - - `site` (NSInteger) - SocialSite enum value - - `host` (NSString) - required for Linkding, optional for others - - `includeTags` (BOOL) - -## Migration from Old Code - -The original `QSDeliciousPlugIn_Source.m` has been refactored into `QSDeliciousPlugIn_Source_New.m`. Key changes: - -1. Removed hardcoded site logic -2. Removed XML parsing from main class (moved to providers) -3. Added strategy pattern implementation -4. Added support for custom hosts (Linkding) -5. Simplified the main object fetching logic - -## Testing - -Tests are included in `QSDeliciousPlugInTests.m` using Swift Testing framework: -- Factory provider selection tests -- Configuration validation tests -- Helper method tests - -## Linkding Configuration - -For Linkding users: -1. Set Site to "Linkding" (value 3) -2. Enter your Linkding server URL in the Host field (e.g., `https://bookmarks.example.com`) -3. Use your API Token as the Password -4. Enter your username (though it's mainly for caching purposes in Linkding) - -The Linkding provider will automatically handle URL construction and JSON parsing. diff --git a/SocialSite.h b/Types/SocialSite.h index f2c848c..f2c848c 100644 --- a/SocialSite.h +++ b/Types/SocialSite.h diff --git a/SocialSite.m b/Types/SocialSite.m index d002235..d002235 100644 --- a/SocialSite.m +++ b/Types/SocialSite.m |