summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.pbxproj52
-rw-r--r--QSDeliciousPlugInTests.m89
-rw-r--r--QSDeliciousPlugIn_Source.h1
-rw-r--r--QSDeliciousPlugIn_Source.m315
-rw-r--r--QSDeliciousPlugIn_Source.xib2
-rw-r--r--REFACTOR_GUIDE.md118
-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