From 6f7a0e57ce0650c9e91b15e75ec9a0bb85f6ddb6 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sat, 6 Sep 2025 23:03:04 +0200 Subject: [PATCH 01/11] Make the provider functional --- QSBookmarkProvider.h | 48 + QSBookmarkProviderFactory.h | 28 + QSBookmarkProviderFactory.m | 66 ++ QSDeliciousAPIProvider.h | 25 + QSDeliciousAPIProvider.m | 190 ++++ QSDeliciousPlugIn.xcodeproj/project.pbxproj | 53 +- QSDeliciousPlugInSource.nib/designable.nib | 930 ------------------- QSDeliciousPlugInSource.nib/keyedobjects.nib | Bin 9343 -> 0 bytes QSDeliciousPlugInTests.m | 89 ++ QSDeliciousPlugIn_Source.h | 23 +- QSDeliciousPlugIn_Source.m | 485 +++++----- QSDeliciousPlugIn_Source.xib | 177 ++++ QSDeliciousPrefPane.h | 13 - QSDeliciousPrefPane.m | 39 - QSLinkdingProvider.h | 16 + QSLinkdingProvider.m | 191 ++++ REFACTOR_GUIDE.md | 118 +++ SocialSite.h | 23 + SocialSite.m | 55 ++ en.lproj/Localizable.strings | 19 +- 20 files changed, 1304 insertions(+), 1284 deletions(-) create mode 100644 QSBookmarkProvider.h create mode 100644 QSBookmarkProviderFactory.h create mode 100644 QSBookmarkProviderFactory.m create mode 100644 QSDeliciousAPIProvider.h create mode 100644 QSDeliciousAPIProvider.m delete mode 100644 QSDeliciousPlugInSource.nib/designable.nib delete mode 100644 QSDeliciousPlugInSource.nib/keyedobjects.nib create mode 100644 QSDeliciousPlugInTests.m create mode 100644 QSDeliciousPlugIn_Source.xib delete mode 100644 QSDeliciousPrefPane.h delete mode 100644 QSDeliciousPrefPane.m create mode 100644 QSLinkdingProvider.h create mode 100644 QSLinkdingProvider.m create mode 100644 REFACTOR_GUIDE.md create mode 100644 SocialSite.h create mode 100644 SocialSite.m diff --git a/QSBookmarkProvider.h b/QSBookmarkProvider.h new file mode 100644 index 0000000..735ae82 --- /dev/null +++ b/QSBookmarkProvider.h @@ -0,0 +1,48 @@ +// +// QSBookmarkProvider.h +// QSDeliciousPlugIn +// +// Protocol for social bookmark providers +// + +#import +#import "SocialSite.h" + +@class QSObject; + +@protocol QSBookmarkProvider + +@required +/** + * Check if this provider can handle the given site configuration + */ +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * 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; + +/** + * Get the supported site for this provider + */ +- (SocialSite)supportedSite; + +/** + * Get display name for this provider + */ +- (NSString *)providerName; + +@optional +/** + * Get bookmarks for a specific tag (used for child loading) + */ +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Get the tag URL type identifier for this provider + */ +- (NSString *)tagURLType; + +@end \ No newline at end of file diff --git a/QSBookmarkProviderFactory.h b/QSBookmarkProviderFactory.h new file mode 100644 index 0000000..e2e694b --- /dev/null +++ b/QSBookmarkProviderFactory.h @@ -0,0 +1,28 @@ +// +// QSBookmarkProviderFactory.h +// QSDeliciousPlugIn +// +// Factory for managing bookmark providers +// + +#import +#import "QSBookmarkProvider.h" +#import "SocialSite.h" + +@interface QSBookmarkProviderFactory : NSObject + +@property (nonatomic, strong, readonly) NSArray> *providers; + ++ (instancetype)sharedFactory; + +/** + * Get the appropriate provider for the given site configuration + */ +- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; + +/** + * Get all available providers + */ +- (NSArray> *)allProviders; + +@end \ No newline at end of file diff --git a/QSBookmarkProviderFactory.m b/QSBookmarkProviderFactory.m new file mode 100644 index 0000000..6fcfdfb --- /dev/null +++ b/QSBookmarkProviderFactory.m @@ -0,0 +1,66 @@ +// +// QSBookmarkProviderFactory.m +// QSDeliciousPlugIn +// + +#import "QSBookmarkProviderFactory.h" +#import "QSDeliciousAPIProvider.h" +#import "QSLinkdingProvider.h" +#import "SocialSite.h" + +@interface QSBookmarkProviderFactory () +@property (nonatomic, strong, readwrite) NSArray> *providers; +@end + +@implementation QSBookmarkProviderFactory + ++ (instancetype)sharedFactory { + static QSBookmarkProviderFactory *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + [self setupProviders]; + } + return self; +} + +- (void)setupProviders { + NSMutableArray *mutableProviders = [NSMutableArray array]; + + // Create Pinboard API providers for each supported site + QSDeliciousAPIProvider *deliciousProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteDelicious]; + QSDeliciousAPIProvider *magnoliaProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteMagnolia]; + QSDeliciousAPIProvider *pinboardProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSitePinboard]; + + // Create Linkding provider + QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; + + [mutableProviders addObject:deliciousProvider]; + [mutableProviders addObject:magnoliaProvider]; + [mutableProviders addObject:pinboardProvider]; + [mutableProviders addObject:linkdingProvider]; + + self.providers = [mutableProviders copy]; +} + +- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + for (id provider in self.providers) { + if ([provider canHandleSite:site username:username password:password host:host]) { + return provider; + } + } + return nil; +} + +- (NSArray> *)allProviders { + return self.providers; +} + +@end diff --git a/QSDeliciousAPIProvider.h b/QSDeliciousAPIProvider.h new file mode 100644 index 0000000..0064264 --- /dev/null +++ b/QSDeliciousAPIProvider.h @@ -0,0 +1,25 @@ +// +// QSDeliciousAPIProvider.h +// QSDeliciousPlugIn +// +// Base provider for Pinboard v1 API (XML-based with Basic Auth) +// Used by Delicious, Magnolia, and Pinboard +// + +#import +#import "QSBookmarkProvider.h" + +@interface QSDeliciousAPIProvider : NSObject + +@property (nonatomic, strong) NSMutableArray *posts; +@property (nonatomic, assign) SocialSite site; + +- (instancetype)initWithSite:(SocialSite)site; + +// Subclasses can override these methods +- (NSString *)apiURLForSite:(SocialSite)site; +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; + +@end diff --git a/QSDeliciousAPIProvider.m b/QSDeliciousAPIProvider.m new file mode 100644 index 0000000..d97d1d2 --- /dev/null +++ b/QSDeliciousAPIProvider.m @@ -0,0 +1,190 @@ +// +// QSDeliciousAPIProvider.m +// QSDeliciousPlugIn +// + +#import "QSDeliciousAPIProvider.h" +#import "SocialSite.h" +#import + +@implementation QSDeliciousAPIProvider + +- (instancetype)initWithSite:(SocialSite)site { + self = [super init]; + if (self) { + _site = site; + _posts = [NSMutableArray array]; + } + return self; +} + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == self.site) && + username.length > 0 && + password.length > 0 && + (site != SocialSiteLinkding); // This provider doesn't handle Linkding +} + +- (SocialSite)supportedSite { + return self.site; +} + +- (NSString *)providerName { + return [SocialSiteHelper displayNameForSite:self.site]; +} + +- (NSString *)apiURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"api.del.icio.us/v1"; + case SocialSiteMagnolia: + return @"ma.gnolia.com/api/mirrord/v1"; + case SocialSitePinboard: + return @"api.pinboard.in/v1"; + default: + return nil; + } +} + +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSString *apiURL = [self apiURLForSite:site]; + if (!apiURL) return nil; + + NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + return [NSURL URLWithString:urlString]; +} + +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSString *)tagURLType { + return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { + + // Try cached data first + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + + // If no cached data, fetch from API + if (![data length]) { + NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; + if (!requestURL) return @[]; + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"POST"]; + [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forSite:site username:username]; + } + + // Parse XML data + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *post in self.posts) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSString *tagString = [post objectForKey:@"tag"]; + if (tagString.length > 0) { + [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + 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 setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + if (!data) return @[]; + + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *post in self.posts) { + NSString *postTags = [post objectForKey:@"tag"]; + if ([postTags rangeOfString:tag].location != NSNotFound) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForPost:(NSDictionary *)post { + QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; + [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; + [newObject setName:[post objectForKey:@"description"]]; + [newObject setDetails:[post objectForKey:@"extended"]]; + [newObject setPrimaryType:QSURLType]; + return newObject; +} + +#pragma mark - NSXMLParserDelegate + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { + if ([elementName isEqualToString:@"post"] && attributeDict) { + [self.posts addObject:attributeDict]; + } +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + // Implementation if needed +} + +@end diff --git a/QSDeliciousPlugIn.xcodeproj/project.pbxproj b/QSDeliciousPlugIn.xcodeproj/project.pbxproj index ce2a3d5..ebe2b52 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -8,30 +8,43 @@ /* Begin PBXBuildFile section */ 28855EDF1207EA17003DC758 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28855EDE1207EA17003DC758 /* Security.framework */; }; - 7F8AD05107F2503600011548 /* QSDeliciousPlugInSource.nib in Resources */ = {isa = PBXBuildFile; fileRef = 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */; }; 7F9441A10803A9D9007EDC31 /* QSObjectSource.name.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7F9441A00803A9D9007EDC31 /* QSObjectSource.name.strings */; }; 7FB0DC1A0B91FF8600A5B6FF /* bookmark_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 7FB0DC190B91FF8600A5B6FF /* bookmark_icon.png */; }; 7FB0DC2F0B91FFC600A5B6FF /* QSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */; }; 7FB0DC300B91FFC600A5B6FF /* QSFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */; }; 8D1AC9700486D14A00FE50C9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD92D38A0106425D02CA0E72 /* Cocoa.framework */; }; + B5CF7D6A2E6B7631008A0EE6 /* QSDeliciousPlugIn_Source.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */; }; + B5CF7D6E2E6B7A53008A0EE6 /* QSDeliciousAPIProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */; }; + B5CF7D6F2E6B7A53008A0EE6 /* QSBookmarkProviderFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */; }; + B5CF7D702E6B7A53008A0EE6 /* QSLinkdingProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */; }; + B5CF7D712E6B7A53008A0EE6 /* SocialSite.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */; }; D486257618B29CCE00D8CAE4 /* del.icio.us.png in Resources */ = {isa = PBXBuildFile; fileRef = D486257518B29CCE00D8CAE4 /* del.icio.us.png */; }; E182BCD306FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h in Resources */ = {isa = PBXBuildFile; fileRef = E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */; }; E182BCD406FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m in Sources */ = {isa = PBXBuildFile; fileRef = E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */; }; - E182BE2506FC9AB5007BF2C2 /* QSDeliciousPrefPane.h in Resources */ = {isa = PBXBuildFile; fileRef = E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */; }; - E182BE2606FC9AB5007BF2C2 /* QSDeliciousPrefPane.m in Sources */ = {isa = PBXBuildFile; fileRef = E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */; }; E182BE3C06FC9B13007BF2C2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E182BE3A06FC9B13007BF2C2 /* Localizable.strings */; }; E182BE5B06FC9C46007BF2C2 /* PreferencePanes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E182BE5A06FC9C46007BF2C2 /* PreferencePanes.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 28855EDE1207EA17003DC758 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; - 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; path = QSDeliciousPlugInSource.nib; sourceTree = ""; }; 7F9441A00803A9D9007EDC31 /* QSObjectSource.name.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = QSObjectSource.name.strings; sourceTree = ""; }; 7FB0DC190B91FF8600A5B6FF /* bookmark_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bookmark_icon.png; sourceTree = ""; }; 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSCore.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSCore.framework; sourceTree = ""; }; 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSFoundation.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSFoundation.framework; sourceTree = ""; }; 8D1AC9730486D14A00FE50C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8D1AC9740486D14A00FE50C9 /* Social Bookmarks Plugin.qsplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Social Bookmarks Plugin.qsplugin"; sourceTree = BUILT_PRODUCTS_DIR; }; + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocialSite.h; sourceTree = ""; }; + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocialSite.m; sourceTree = ""; }; + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProvider.h; sourceTree = ""; }; + B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSDeliciousAPIProvider.h; sourceTree = ""; }; + B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousAPIProvider.m; sourceTree = ""; }; + B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSLinkdingProvider.h; sourceTree = ""; }; + B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSLinkdingProvider.m; sourceTree = ""; }; + B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProviderFactory.h; sourceTree = ""; }; + B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSBookmarkProviderFactory.m; sourceTree = ""; }; + B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugInTests.m; sourceTree = ""; }; + B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTOR_GUIDE.md; sourceTree = ""; }; + B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QSDeliciousPlugIn_Source.xib; sourceTree = ""; }; D475F9AF18B2992D0012243C /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; D475F9B018B2992D0012243C /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; D475F9B118B2992D0012243C /* Developer.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Developer.xcconfig; sourceTree = ""; }; @@ -42,9 +55,7 @@ DD92D38A0106425D02CA0E72 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; E182BCCF06FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPlugIn_Source.h; sourceTree = ""; }; E182BCD006FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugIn_Source.m; sourceTree = ""; }; - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = QSDeliciousPrefPane.h; sourceTree = ""; }; - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPrefPane.m; sourceTree = ""; }; - E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E182BE3B06FC9B13007BF2C2 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E182BE5A06FC9C46007BF2C2 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -118,12 +129,21 @@ 32DBCF9F0370C38200C91783 /* Other Sources */ = { isa = PBXGroup; children = ( + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */, + B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */, + B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */, + B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */, + 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 */, - 7F8AD05007F2503600011548 /* QSDeliciousPlugInSource.nib */, - E182BE2206FC9AB5007BF2C2 /* QSDeliciousPrefPane.h */, - E182BE2306FC9AB5007BF2C2 /* QSDeliciousPrefPane.m */, ); name = "Other Sources"; sourceTree = ""; @@ -171,6 +191,11 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0500; + TargetAttributes = { + 8D1AC9600486D14A00FE50C9 = { + ProvisioningStyle = Manual; + }; + }; }; buildConfigurationList = 7F07AFAE085E432E00E2AFC4 /* Build configuration list for PBXProject "QSDeliciousPlugIn" */; compatibilityVersion = "Xcode 3.2"; @@ -199,9 +224,8 @@ files = ( E182BCD306FC8203007BF2C2 /* QSDeliciousPlugIn_Source.h in Resources */, D486257618B29CCE00D8CAE4 /* del.icio.us.png in Resources */, - E182BE2506FC9AB5007BF2C2 /* QSDeliciousPrefPane.h in Resources */, E182BE3C06FC9B13007BF2C2 /* Localizable.strings in Resources */, - 7F8AD05107F2503600011548 /* QSDeliciousPlugInSource.nib in Resources */, + B5CF7D6A2E6B7631008A0EE6 /* QSDeliciousPlugIn_Source.xib in Resources */, 7F9441A10803A9D9007EDC31 /* QSObjectSource.name.strings in Resources */, 7FB0DC1A0B91FF8600A5B6FF /* bookmark_icon.png in Resources */, ); @@ -231,8 +255,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B5CF7D6E2E6B7A53008A0EE6 /* QSDeliciousAPIProvider.m in Sources */, + B5CF7D6F2E6B7A53008A0EE6 /* QSBookmarkProviderFactory.m in Sources */, + B5CF7D702E6B7A53008A0EE6 /* QSLinkdingProvider.m in Sources */, + B5CF7D712E6B7A53008A0EE6 /* SocialSite.m in Sources */, E182BCD406FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m in Sources */, - E182BE2606FC9AB5007BF2C2 /* QSDeliciousPrefPane.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QSDeliciousPlugInSource.nib/designable.nib b/QSDeliciousPlugInSource.nib/designable.nib deleted file mode 100644 index 030965a..0000000 --- a/QSDeliciousPlugInSource.nib/designable.nib +++ /dev/null @@ -1,930 +0,0 @@ - - - - 1060 - 10F569 - 762 - 1038.29 - 461.00 - - YES - - YES - - - YES - - - - YES - - - - - YES - - - YES - - - - YES - - QSDeliciousPlugIn_Source - - - FirstResponder - - - NSApplication - - - 15 - 2 - {{62, 510}, {270, 151}} - 1886912512 - - Window - - NSWindow - - View - - {1.79769e+308, 1.79769e+308} - {213, 107} - - - 256 - - YES - - - 266 - {{89, 85}, {150, 19}} - - YES - - -1804468671 - 4326400 - - - LucidaGrande - 11 - 3100 - - - YES - - 6 - System - textBackgroundColor - - 3 - MQA - - - - 6 - System - textColor - - 3 - MAA - - - - - - - 264 - {{6, 88}, {78, 14}} - - YES - - 67239424 - 71434240 - Name: - - - - 6 - System - controlColor - - 3 - MC42NjY2NjY2NjY3AA - - - - 6 - System - controlTextColor - - - - - - - 264 - {{6, 61}, {78, 14}} - - YES - - 67239424 - 71434240 - Password: - - - - - - - - - 266 - {{89, 58}, {150, 19}} - - YES - - -1804468671 - 4326400 - - - - YES - - - - - - - 264 - {{86, 34}, {87, 18}} - - YES - - 67239424 - 131072 - Include tags - - - 1211912703 - 2 - - NSSwitch - - - - 200 - 25 - - - - - 264 - {{86, 113}, {126, 26}} - - YES - - -2076049856 - 2048 - - LucidaGrande - 13 - 1044 - - - 109199615 - 1 - - LucidaGrande - 13 - 16 - - - - - - 400 - 75 - - - del.icio.us - - 1048576 - 2147483647 - 1 - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - _popUpItemAction: - - - YES - - - OtherViews - - - YES - - - - ma.gnolia - - 1048576 - 2147483647 - - - _popUpItemAction: - 1 - - - - - pinboard.in - - 1048576 - 2147483647 - - - _popUpItemAction: - 2 - - - - - 3 - YES - YES - 1 - - - - - 264 - {{6, 119}, {78, 14}} - - YES - - 67239424 - 71434240 - Site: - - - - - - - - {270, 151} - - - {{0, 0}, {1024, 746}} - {213, 129} - {1.79769e+308, 1.79769e+308} - - - - YES - username - includeTags - site - - YES - - - - - - YES - - - nextKeyView - - - - 56 - - - - nextKeyView - - - - 57 - - - - userField - - - - 65 - - - - passField - - - - 66 - - - - settingsView - - - - 69 - - - - value: selection.username - - - - - - value: selection.username - value - selection.username - 2 - - - 82 - - - - value: selection.includeTags - - - - - - value: selection.includeTags - value - selection.includeTags - 2 - - - 84 - - - - value: currentPassword - - - - - - value: currentPassword - value - currentPassword - 2 - - - 85 - - - - contentObject: selection.info - - - - - - contentObject: selection.info - contentObject - selection.info - 2 - - - 91 - - - - selectedTag: selection.site - - - - - - selectedTag: selection.site - selectedTag - selection.site - 2 - - - 98 - - - - - YES - - 0 - - - - - - -2 - - - File's Owner - - - -1 - - - First Responder - - - 5 - - - YES - - - - Window - - - 6 - - - YES - - - - - - - - - - - - 50 - - - YES - - - - - - 51 - - - YES - - - - - - 52 - - - YES - - - - - - 53 - - - YES - - - - - - 70 - - - YES - - - - - - 92 - - - YES - - - - - - 99 - - - YES - - - - - - 75 - - - Settings - - - 101 - - - - - 102 - - - - - 103 - - - - - 104 - - - - - 105 - - - - - 106 - - - YES - - - - - - 107 - - - - - 93 - - - YES - - - - - - - - 95 - - - - - 94 - - - - - -3 - - - Application - - - 108 - - - - - - - YES - - YES - 108.ImportedFromIB2 - 5.IBEditorWindowLastContentRect - 5.IBWindowTemplateEditedContentRect - 5.ImportedFromIB2 - 5.windowTemplate.hasMinSize - 5.windowTemplate.minSize - 50.ImportedFromIB2 - 51.ImportedFromIB2 - 52.ImportedFromIB2 - 53.CustomClassName - 53.ImportedFromIB2 - 6.ImportedFromIB2 - 70.ImportedFromIB2 - 75.ImportedFromIB2 - 92.ImportedFromIB2 - 93.IBEditorWindowLastContentRect - 93.ImportedFromIB2 - 94.ImportedFromIB2 - 95.ImportedFromIB2 - 99.ImportedFromIB2 - - - YES - - {{0, 994}, {270, 151}} - {{0, 994}, {270, 151}} - - - {213, 107} - - - - NSSecureTextField - - - - - - {{75, 1070}, {145, 63}} - - - - - - - - YES - - - YES - - - - - YES - - - YES - - - - 108 - - - - YES - - FirstResponder - NSObject - - IBUserSource - - - - - NSObject - - IBUserSource - - - - - QSDeliciousPlugIn_Source - QSObjectSource - - YES - - YES - passField - userField - - - YES - NSTextField - NSTextField - - - - IBProjectSource - QSDeliciousPlugIn_Source.h - - - - QSDeliciousPlugIn_Source - QSObjectSource - - savePassword: - id - - - IBUserSource - - - - - QSObjectSource - NSObject - - IBUserSource - - - - - - YES - - NSApplication - - relaunch: - id - - - IBFrameworkSource - QSFoundation.framework/Headers/NSApplication_BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSIconLoader.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObject.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObjectSource.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSObject_FileHandling.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSPlugIn.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSProxyObject.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSRegistry.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSTask.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSTriggerCenter.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/QSURLDownloadWrapper.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKFileWatcher.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKKQueue.h - - - - NSObject - - IBFrameworkSource - QSCore.framework/Headers/UKMainThreadProxy.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NDHotKeyEvent.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSArray_BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSObject+BLTRExtensions.h - - - - NSObject - - IBFrameworkSource - QSFoundation.framework/Headers/NSObject+ReaperExtensions.h - - - - NSView - - IBFrameworkSource - QSFoundation.framework/Headers/NSView_BLTRExtensions.h - - - - NSWindow - - IBFrameworkSource - QSFoundation.framework/Headers/NSWindow_BLTRExtensions.h - - - - QSObjectSource - NSObject - - settingsView - NSView - - - - - - 0 - IBCocoaFramework - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 - - - YES - - 3 - - YES - - YES - NSMenuCheckmark - NSMenuMixedState - - - YES - {9, 8} - {7, 2} - - - - diff --git a/QSDeliciousPlugInSource.nib/keyedobjects.nib b/QSDeliciousPlugInSource.nib/keyedobjects.nib deleted file mode 100644 index a794dc2c40e8deec0aa197bded99e6ce695b534a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9343 zcmai3349aP*1z{o_B6?4Pnszf3jzYQElUAG=n4f|3Mo)%A*9LBrld(tQYe(dMHUg+ zL3R+z;=Vk6E{Hxi1XNtu1i`019?Ek^C z7>LF)GL9pR2x5>7$&rFt!rWipX*wT?2Ew88PT{Ir-WQ8b=;V#~Y65V(tWzvpZ$fy{ z_NUkrNQqRaC(1-ws2}Q&2B92Og2tf$szuXK13aCM7Nh&nDzq9sfF4BK(GIi+J%XM@ zPoZbf3+N@Z550=^qxaECbPAnC=g|f9Bl;Enh7rbChSgYu&Derda0lE4cg2J7T{su# z;X*tDm*DZZ9N&$n<0d>8&&TWW7Q7X=;GK9Eeh5E?AIDGNr}49RFMbOjzz6YBd<-AQ zr|=p4FMJMvhrh@F!#^V>{ssSre`I^d)za zT#`rfNdXy2ib)9>MaGfwq?~xj6yhbb$OB{@*+@2#tz;W{kZdPANDJ9b9wv{F$H;#2 zDfyZFLVhK`k>AN5SmEgT8~o&nlmCRh=-s?~}T zFe1K)_UhwP!x2GFVK@{k_J+LGd<3qNe;|@1OeEd<=3-v~Q zP~VBt0z)vRT>8pKCxe`ZXe?YOEvE&>WtS%w2jM8n%?ta&-c}b21`I%VqJiL?^mv9- z<#tKOU1%_Pw74PWh4{Jo*iLj88iKONmbl?Unwg98==eubJ}N+kXeb(niqLR00*yq) zR82clALVH^t)UHcCT*tlIA|M%O3`RE2DwogN*!O~9_g*(gW$P*J{k*zyh1?0U}>o| zmEJ-%l$=K6Q8}7`CW5mnP$fv2geD^onu5Hj3i*&9{!c~Ks0M;v5E88QgJf}$fT27- z7zF>i8|wMU^Z-8t#7{2_daI?l3*Z>TN9)5OKOccd5>7p=8N3z=RD(&u*2t)-p0ZR& zwMMLgj}Fv8lRT_Z@Q1GK>IAEOzl5zR!i zKt&UpjhfLMG#61c4_rGREkFywMfVEG0tbi-hG@2a&jNh_zh^`2{(lN;=OB^;tcau_*r}Rl^hewl_6?`_@&Tf zYEn~csSy7q=zg>mEkn!E3bYcUBOo3=1P_4sr;2t`3pJ+=xebXw#w4zCWP>l@_YRGC zAxIK7*Pyj9OGv&Jv<79LK#xyWj}n zySv`&;{ht-1+`HpwX+K980VfDjq!B=f*6Ff-2#t+q7A48l6UPk{w*&qa-wcc%;#IHl*r9zfgK{k*?ML0mmKZFjVL+EY5;k)QCdXJ{j zE;Iv>(3=jT6{lLWxdL2S30dJmAEF~5>KHl>|Etxs6YWfOLXzDEf(@HT>Uz$Fx>FO6#vh|K1HOJ z+D))rm9X60)YP{}YCkb?v-?5b_HJ%I)h=)6po{1ybO}5*AN>!q_7{^3UqL2WE<*Hn z@LUQ_rzYBctbp7B?U8;3z;smvQxDp+(kI#89#5E|&f<*pzKNe~)J_amwqo)Q%0Z)u z3lcCV3cJbDaT3;{Dr`tBJ(FhLWa)h~g_!;6(yu+z(5ts#NWW}|LBHnaf8uUv2EH9N;_kQy?isJ5 zvvE4U183k&oP~PUPCU@Vno8WId?XO|2Yf|PJ0sJ*Lhs6Lh{eL8 zcu#@GkrRYR(93uMNf7A>8EeMf@PzB)I15k2 z6}S=!k(`7lV-NV?4m<_z!d_g3eGpL|Peq4u4f5bxJPik-_;_#_*W<9{{|G;ok3@KX zfxlX4$;EuAp-Ain$=5D~f&knxCm5&>)j=?!>5F=ixR!}F;Pu2{a9(kUuM39)zL8-c0I)dF$os|NAfPM& z?TQwgVC#awQ9}VWCDdq|3j~CYpoPcq1~73c-iSBh&5#0#P)2DfEvKU@{CqH7;MeI5 zQE0t*8%oCy;_Y|`WE})p}$dw!8_=1I+6|(hYI$H>xm=7=&29akF6KLBl&HBB>GAGR2v|+;3tKglmht- z&6_wXR>MbxP9@|!kojEv938(C*WefMi!`5(re#vpUb-P_`|!)xMePlU+J0d7W!`G) z{u8f`nNa6VuMUNS0WXC028;hp)gk;g9@qvTAAqVObljgP8Cy{w2vvo>5r29hbTc)Q zV@}|YAXag}LQ;8W?!+hXNt6x25-+7;ALG;U1Z}||C+gWJ_@5#yO9hu-D{Fi3C-_q+ zZJ*)KMdFjdj8O9alxg9!_}_rkHlp^xt-maIU*hvn@V-j4&PqDzy4DFT3u;^@ko>>w zoj>3U_($~KO?u~KT9N3TW8J_{2TW|r$`-Z_eKMOxub?mRuRA5mQyJxBu=T2r3Lt93 z?tp_F6xp30_#2tf5alBxZEQ1y$bjM<1Ohk}$X!}R2-pIoZaXYY=%C}oL7d1$l8Fm>NDAoy@W|(VK`+#EkQ;?GD)0xy?JVT62e2b<-2J7IaN|r` ztETld6i*`35t4}9O43LtI*kTtUAeISkg$&kNG%q!Bw7YFoIe5@yV0=dR??loc}RQF zEu<&PzVWs+;|`J`HV$dWE-(hzhXwh_PLfHoglWJz+CKv*UK{9PT~R&=-r@aV(|}az z0$NMDU|mI1^5rT4n(ldIN%h zYBliJOBdOZ5W2DALs;51pR@^Vdo87LEp9jlE2DEoEpuQk8V_rWYk?Dh2ylS}Q-yH1 zyVG9ggt*K~u>2ssN0d1aEKh?2uNy2+pby}1&EUjM5woO<_~@#qSQ`F=5(V3P(OC1Dh z-38d?V7h{4d`g2Tkhz4Cd4T$R$b7PZEF_D_y<{=Dk1Rn|WGPujmXj4^C9G~0 zSxwfEwSZRXRAmf+6bhwuA$D)b<6>{4u=#HtBvcRKb|56&2LC1|2#5yGdVHBk)mtqQx5z*c1kK`iu`@`s+_9NKULY)VAzeh5fwz{@ z`{@02IbC9^Zv@_PFI^1z&f1}3XTb?gW`;?BSvYTPKO-)eEMO~TwvxLH)nGL&|#F(u9 zFy=3%Nx8q1>=LPnWKbDjS04nD+Cp}U6a!A%?jR4rapX#HRZcy;Bk+mmbK+cll04cX zG}{FDo)EzYx$Xye!Lb0qtO~aOZ=?61@7DqlYXy=Y|2+)Ps~~?C0KDMn0AQvAm}LME z3*a{m{$K|X{~ZL+bqOs81TFTq5Qoj*K>^|b?^=+0Sb(+waft!l0P?;dT>yD!&_Mv; z0=NZ`i%dy?c|z0YlBR!=v*h389QlHLNzRl1kgv$sd2o2}CjAOBDxn72A5ZayW_=n>S##-Nc@m9yVE`HgQ31 zH;3IOUKjANh`M%7TxB48#>ypqD>~isOw^K&1rEIF-E#-2XqWg;@T=)`=~Z??G#Yf5l+8G z0FQ$MbfFgjqJ@JqFB*=DVXImT$Ln9i!TLxM-G5GoP+%{k};24zE<0#ms%cXo_+r zQj!lR$1Tiak++Ggl=tVvUwj^6mNLsG73IcXnD}8N#qj#m3Gc_c!yA_YXfXa3Uj*_q zp45^?vIJfTZY6ukL2{g&fkJYDTwz#-WBP$p%fNBv;Iv9+GQ8lcf|q+!;l*ApywnT9 z3%v-u%$p7`^5!wim=(+_=5gjZ<~8PR<`nZ!=1b-m8IkE^1{o`J%5IbOl=YGgl-(tB z%f`ya%O=PwWRqkbnOEkM@v>@Jqm0Uy%ht;_$~Mck${v&*k{y+umik@<#ar`3m_e`5O5;`3CtW`4;&$`SbEK^7Hab z@~aA+!l_78WGhA}$`lh66^cm;kHV|)Dg275ifTopVy6S1OfirB5otc)rflrxkwl}*Zp z%GJuX%Js_a%4d}Ol!ufbC{HQRE5B7H*aT)h5*z)i%}hsxzu{s;^YvsJ>JEp!!kulj@S{vg)c@ zqqeAB>MrVG>LT?Bb+LMsdW^bEJx*P&4yjkD*Qz(FH>Khppt)6(r^(k8YKCcsYl=0aG@~_cO+d3mvqH00vre-? zvq`f>vrV&I^Qh)s&0)>^nh!NcH77JDH6LrvXfA5IXa{PCYo}-<+J)Nn+CAEbwNGfD z(Y~PFtKFx4Mfx(&LGy3M++y6w6hx)$9o-5%Wwy1lx6y8XI?x>LH-x=(ap=+5gd=zi7xuKPo; z&^z?E=)3C2>L=?X`dRt~`ephJ`aSw*_51X%=-<%q*B{Uy(!ZlWtUsbZraz%StN&hq zL4Q$y$$$(x183-9NHyGQxZN<&P+%xGOfu9Of`*Wx-Z0;=-mu;9nBi%|Uc)}a0mBD| zBZgCk(}qtBpR!uk$eLLPo6K@-2R4cIW4lA4C}O9xjchYJmz~GXXBV>fvP;?J z>`HbE`!u^3PD(#uPqLr0=h<)B{~Faso3XPo%h=zTYb-TRGR`s1HO@26H!d_THZCzP zH7+;qHXbm(Yy8l7)Og(Zk@1xAwDA+;S>t!c%f>%U8k5VEV!FlD(UfNDX}Zfa#FT5w zHx-)3n0zMQRBZ~FrkNI-R-682+Gl#j^s4D~)0?KZOb1P;Oy^A(%`&sXtTe05TC>G$ zGpCquF?TelnR}S~nFpD3&H3hHv&ZZ=PdCpsuQhKrKW%=+e8zmCZ6w!Clo&~nsr-13v<7pu{lZq2Y}S$kXiTKicC zSlw3MdXII1b&++kb%}MUb-DFX>#Np7)_1JGSbwwrVZCa@HpV8mDQ#+-)~2%=Y(|^e zX0_REPMgb?V!OrG(UxZGY@2MGW}9i7Z(C{GWP8^3n(brTH@5%U8N0!rX76n8V((_} zZtrQo!=7pHW$$AjU>|6|%Ra-yB&XGBcMfw7caC(HI7^*loMp~&&T{8OXQgwpbBeRd>32?b);MdOL1)NW?~FKO z&gsr(=Mv{G=S$8vohO}_oL7@mlCzVGk|!nCCO0OpNq#>0o#fAxe|4E$JzV`H?qTjx?s4u(?rH8B?(f|5+>6{x+{@fQxYxKhxc%G#?gQ=!cZ@s1o#Z~| w&T!|rFS+yF1@0pE6ZboJg}a)fO0lPKDd{P_QgWmNYC@z>X1Vm4_@9#Ze+h#rwEzGB diff --git a/QSDeliciousPlugInTests.m b/QSDeliciousPlugInTests.m new file mode 100644 index 0000000..3aff4ff --- /dev/null +++ b/QSDeliciousPlugInTests.m @@ -0,0 +1,89 @@ +// +// QSDeliciousPlugInTests.m +// QSDeliciousPlugIn +// + +#import +#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 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 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 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 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 89729de..9d22d91 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -6,17 +6,20 @@ // Copyright __MyCompanyName__ 2004. All rights reserved. // +#import +#import +#import "SocialSite.h" +#import "QSBookmarkProvider.h" +#import "QSBookmarkProviderFactory.h" -#import "QSDeliciousPlugIn_Source.h" - -@interface QSDeliciousPlugIn_Source : QSObjectSource { - NSMutableArray *posts; - NSMutableArray *tags; - NSMutableArray *dates; - - IBOutlet NSTextField *userField; - IBOutlet NSTextField *passField; +@interface QSDeliciousPlugIn_Source : QSObjectSource { + IBOutlet NSTextField *userField; + IBOutlet NSTextField *passField; + IBOutlet NSTextField *hostField; } - @end +@interface QSCatalogEntry (OldStyleSourceSupport) +@property NSMutableDictionary *info; +- (id)objectForKey:(NSString *)key; +@end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 8dcc9b4..43a32cb 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -8,126 +8,143 @@ #import "QSDeliciousPlugIn_Source.h" #import - #import @implementation QSDeliciousPlugIn_Source + (void)initialize { - [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; + [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; } - (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { - return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; + return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } -- (BOOL)isVisibleSource{ return YES; } +- (BOOL)isVisibleSource{ + return YES; +} - (NSImage *) iconForEntry:(NSDictionary *)dict { return [[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]; } -- (NSString *) mainNibName { - return @"QSDeliciousPrefPane"; +- (NSView *)settingsView +{ + if (![super settingsView]) { + [[NSBundle bundleForClass:[self class]] loadNibNamed:NSStringFromClass([self class]) owner:self topLevelObjects:NULL]; + } + return [super settingsView]; +} + +#pragma mark - Settings Helpers + +- (SocialSite)siteIndex { + NSDictionary *settings = self.selectedEntry.sourceSettings; + return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : SocialSiteDelicious; +} + +- (NSString *)currentUsername { + return [self.selectedEntry.sourceSettings objectForKey:@"username"]; +} + +- (NSString *)currentHost { + return [self.selectedEntry.sourceSettings objectForKey:@"host"]; } -- (void)populateFields { - NSLog(@"populating: %@/%@", [self.selectedEntry.sourceSettings objectForKey:@"username"], [self.selectedEntry.sourceSettings objectForKey:@"site"]); +- (NSString *)currentPassword { + return [self.selectedEntry.sourceSettings objectForKey:@"password"]; } -- (NSView *) settingsView { - if (![super settingsView]) { - [[NSBundle bundleForClass:[self class]] loadNibNamed:@"QSDeliciousPlugInSource" owner:self topLevelObjects:NULL]; - } - return [super settingsView]; +- (BOOL)includeTags { + return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] boolValue]; } -// Keychain Access -- The QS Built-in ones seems to be broken +#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; + 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; } - (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; + 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 *)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; + 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; } - (NSString *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; + return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; } - (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, + 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], @@ -138,218 +155,130 @@ - (OSErr)addURLPasswordToKeychain:(NSURL *)url { (UInt32)[pass length], [pass UTF8String], NULL); } - - return err; -} - -// Site Index/API/URL - -- (NSInteger)siteIndex { - NSDictionary *settings = self.selectedEntry.sourceSettings; - return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : 0; -} - -- (NSString *)siteURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"del.icio.us"; - else if (siteIndex == 1) return @"ma.gnolia.com"; - else if (siteIndex == 2) return @"pinboard.in"; - else return nil; -} - -- (NSString *)reversedSiteURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"us.icio.del"; - else if (siteIndex == 1) return @"com.gnolia.ma"; - else if (siteIndex == 2) return @"in.pinboard"; - else return nil; + + return err; } -- (NSString *)tagURLForIndex:(NSInteger)siteIndex { - return [NSString stringWithFormat:@"tag.%@", [self reversedSiteURLForIndex:[self siteIndex]]]; -} - -- (NSString *)apiURLForIndex:(NSInteger)siteIndex { - if (siteIndex == 0) return @"api.del.icio.us/v1"; - else if (siteIndex == 1) return @"ma.gnolia.com/api/mirrord/v1"; - else if (siteIndex == 2) return @"api.pinboard.in/v1"; - else return nil; -} - -// Current Site/API URL - -- (NSString *)currentSiteURL { - return [self siteURLForIndex:[self siteIndex]]; -} - -- (NSString *)currentAPIURL { - return [self apiURLForIndex:[self siteIndex]]; -} - -// Useranme - -- (NSString *)currentUsername { - return [self.selectedEntry.sourceSettings objectForKey:@"username"]; -} - -// Password Related - -- (NSString *)currentPassword { - NSString *account = [self currentUsername]; - if (!account) return nil; - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, [self currentSiteURL]]]; - NSString *password = [self keychainPasswordForURL:keychainURL]; - - return password; +- (NSString *)oldCurrentPassword { + NSString *account = [self currentUsername]; + if (!account) return nil; + + SocialSite site = [self siteIndex]; + NSString *host = nil; + + // 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]; + } + + 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; - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@%@/", account, newPassword, [self currentSiteURL]]]; - - [self addURLPasswordToKeychain:keychainURL]; -} - -// Bookarmk/Caching - -- (NSData *)cachedBookmarkData { - NSString *cachePath=[QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", [self currentSiteURL]], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", [self currentUsername]]]; - return [NSData dataWithContentsOfFile:cachePath]; -} - -- (NSData *)bookmarkData { - if (![self currentUsername] || ![self currentPassword]) return nil; - - NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", [self currentUsername], [self currentPassword], [self currentAPIURL]]; - NSURL *requestURL = [NSURL URLWithString:urlString]; - - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"POST"]; - [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; - [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - - NSError *error; - NSData *data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", [self currentSiteURL]], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", [self currentUsername]]]; - [data writeToFile:cachePath atomically:NO]; - - return data; + NSString *account = [self currentUsername]; + if (!account) return; + if ([newPassword length] <= 0) return; + + SocialSite site = [self siteIndex]; + NSString *host = nil; + + // 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]; } -- (QSObject *)objectForPost:(NSDictionary *)post { - QSObject *newObject=[QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; - [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; - [newObject setName:[post objectForKey:@"description"]]; - [newObject setDetails:[post objectForKey:@"extended"]]; - [newObject setPrimaryType:QSURLType]; - //NSDate *date=[NSCalendarDate dateWithString:[post objectForKey:@"time"] - // calendarFormat:@"%Y-%m-%dT%H:%M:%SZ"]; - //[date setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - //[newObject setObject:date forMeta:kQSObjectCreationDate]; - return newObject; -} +#pragma mark - Objects For Entry - (NSArray *)objectsForEntry:(NSDictionary *)theEntry { - NSData *data = nil;//[self cachedBookmarkData]; - if (![data length]) data = [self bookmarkData]; - - NSXMLParser *postParser = [[NSXMLParser alloc]initWithData:data]; - [postParser setDelegate:self]; - - posts = [NSMutableArray arrayWithCapacity:1]; - - [postParser parse]; - - NSMutableArray *objects=[NSMutableArray arrayWithCapacity:1]; - QSObject *newObject; - NSEnumerator *e=[posts objectEnumerator]; - NSDictionary *post; - NSMutableSet *tagSet=[NSMutableSet set]; - while(post=[e nextObject]){ - newObject=[self objectForPost:post]; - [tagSet addObjectsFromArray:[[post objectForKey:@"tag"]componentsSeparatedByString:@" "]]; - [objects addObject:newObject]; - } - NSString *tag; - e=[tagSet objectEnumerator]; - - if ([[theEntry objectForKey:@"includeTags"]boolValue]){ - while(tag=[e nextObject]){ - newObject=[QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[del.icio.us tag]:%@",tag]]; - [newObject setObject:tag forType:[self tagURLForIndex:[self siteIndex]]]; - [newObject setObject:[self currentUsername] forMeta:@"us.icio.del.username"]; - [newObject setName:tag]; - [newObject setPrimaryType:[self tagURLForIndex:[self siteIndex]]]; - [objects addObject:newObject]; - } - } - [postParser release]; - - return objects; + NSLog(@"WE HAVE BEEN REQUESTED"); + SocialSite site = [self siteIndex]; + NSString *username = [self currentUsername]; + NSString *password = [self currentPassword]; + NSString *host = [self currentHost]; + BOOL includeTags = [self includeTags]; + // Get the appropriate provider using the factory + QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; + id 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]; } -- (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username{ - NSData *data=[self cachedBookmarkData]; - - NSXMLParser *postParser=[[NSXMLParser alloc]initWithData:data]; - [postParser setDelegate:self]; - posts=[NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects=[NSMutableArray arrayWithCapacity:1]; - QSObject *newObject; - NSEnumerator *e=[posts objectEnumerator]; - NSDictionary *post; - // NSMutableSet *tagSet=[NSMutableSet set]; - while(post=[e nextObject]){ - if ([[post objectForKey:@"tag"]rangeOfString:tag].location==NSNotFound)continue; - newObject=[self objectForPost:post]; - [objects addObject:newObject]; - } - return objects; -} - -// Object Handler Methods - -/* -- (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:nil]; // An icon that is either already in memory or easy to load +- (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { + SocialSite site = [self siteIndex]; + NSString *password = [self currentPassword]; + NSString *host = [self currentHost]; + + // Get the appropriate provider using the factory + QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; + id 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 @[]; + } + + // Check if provider supports tag-based fetching + if ([provider respondsToSelector:@selector(fetchBookmarksForTag:site:username:password:host:)]) { + return [provider fetchBookmarksForTag:tag site:site username:username password:password host:host]; + } + + return @[]; } -- (BOOL)loadIconForObject:(QSObject *)object { - return NO; - id data=[object objectForType:QSDeliciousPlugInType]; - [object setIcon:nil]; - return YES; -} -*/ +#pragma mark - Object Handler Methods - (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; + [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; } - (BOOL)loadChildrenForObject:(QSObject *)object { - [object setChildren:[self objectsForTag:[object objectForType:[self tagURLForIndex:[self siteIndex]]] - username:[object objectForMeta:@"us.icio.del.username"]]]; - return YES; -} - -// NSXMLParserDelegate Functions - -- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { - NSLog(@"XML Parser started %@ %@ %@ %@",elementName,namespaceURI,qName,attributeDict); - if ([elementName isEqualToString:@"post"] && attributeDict) - [posts addObject:attributeDict]; -} - -- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - NSLog(@"XML Parser ended %@ %@ %@", elementName, namespaceURI, qName); + SocialSite site = [self siteIndex]; + + NSString *tagType = nil; + if (site == SocialSiteLinkding) { + tagType = @"tag.linkding"; + } else { + NSString *reversedURL = [SocialSiteHelper reversedSiteURLForSite:site]; + 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; + + NSArray *children = [self objectsForTag:tag username:username]; + [object setChildren:children]; + return YES; } @end diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib new file mode 100644 index 0000000..65d8dda --- /dev/null +++ b/QSDeliciousPlugIn_Source.xib @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + info.username + info.site + info.includeTags + info.host + + + + + + + + diff --git a/QSDeliciousPrefPane.h b/QSDeliciousPrefPane.h deleted file mode 100644 index 494a9ff..0000000 --- a/QSDeliciousPrefPane.h +++ /dev/null @@ -1,13 +0,0 @@ - - -#import -#import - - -@interface QSDeliciousPrefPane : NSPreferencePane { - IBOutlet NSTextField *userField; - IBOutlet NSTextField *passField; -} - -- (IBAction)savePassword:(id)sender; -@end diff --git a/QSDeliciousPrefPane.m b/QSDeliciousPrefPane.m deleted file mode 100644 index f0dde4e..0000000 --- a/QSDeliciousPrefPane.m +++ /dev/null @@ -1,39 +0,0 @@ - - -#import "QSDeliciousPrefPane.h" -#import - -@implementation QSDeliciousPrefPane -- (id)init { - self = [super initWithBundle:[NSBundle bundleForClass:[QSDeliciousPrefPane class]]]; - if (self) { - } - return self; -} - -- (NSImage *) icon{ - return [[NSBundle bundleForClass:[self class]] imageNamed:@"del.icio.us"]; -} - -- (NSString *) mainNibName{ - return @"QSDeliciousPrefPane"; -} - -- (void)awakeFromNib{ - NSString *account=[[NSUserDefaults standardUserDefaults] objectForKey:@"QSDeliciousUserName"]; - NSURL *url=[NSURL URLWithString:[NSString stringWithFormat:@"http://%@@del.icio.us/",account]]; - NSString *password=[url keychainPassword]; - if (account)[userField setStringValue:account]; - if (password)[passField setStringValue:password]; -} - -- (IBAction)savePassword:(id)sender{ - NSString *account=[userField stringValue]; - NSString *pass=[passField stringValue]; - - NSURL *url=[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%@@del.icio.us/",account,pass]]; - if ([pass length]) - [url addPasswordToKeychain]; -} - -@end diff --git a/QSLinkdingProvider.h b/QSLinkdingProvider.h new file mode 100644 index 0000000..28807e8 --- /dev/null +++ b/QSLinkdingProvider.h @@ -0,0 +1,16 @@ +// +// QSLinkdingProvider.h +// QSDeliciousPlugIn +// +// Provider for Linkding API (JSON-based with API key) +// + +#import +#import "QSBookmarkProvider.h" + +@interface QSLinkdingProvider : NSObject + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username; + +@end \ No newline at end of file diff --git a/QSLinkdingProvider.m b/QSLinkdingProvider.m new file mode 100644 index 0000000..f97f5c7 --- /dev/null +++ b/QSLinkdingProvider.m @@ -0,0 +1,191 @@ +// +// QSLinkdingProvider.m +// QSDeliciousPlugIn +// + +#import "QSLinkdingProvider.h" +#import "SocialSite.h" +#import + +@implementation QSLinkdingProvider + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == SocialSiteLinkding) && + username.length > 0 && + password.length > 0 && // password is API token for Linkding + host.length > 0; +} + +- (SocialSite)supportedSite { + return SocialSiteLinkding; +} + +- (NSString *)providerName { + return @"Linkding"; +} + +- (NSString *)tagURLType { + return @"tag.linkding"; +} + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host includeTags:(BOOL)includeTags { + + if (![self canHandleSite:site username:username password:password host:host]) { + return @[]; + } + + // Try cached data first + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + + // If no cached data, fetch from API + if (![data length]) { + // Construct Linkding API URL + NSString *baseURL = host; + if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { + baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; + } + if ([baseURL hasSuffix:@"/"]) { + baseURL = [baseURL substringToIndex:[baseURL length] - 1]; + } + + NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; + NSURL *requestURL = [NSURL URLWithString:urlString]; + + if (!requestURL) { + NSLog(@"Invalid Linkding host URL: %@", host); + return @[]; + } + + NSLog(@"WE ARE ABOUT TO REQUEST TO URL: %@", requestURL); + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"GET"]; + [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forHost:host username:username]; + } + + // Parse JSON data + NSError *jsonError = nil; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) { + NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); + return @[]; + } + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) { + NSLog(@"Invalid Linkding response format"); + return @[]; + } + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *bookmark in results) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]]) { + [tagSet addObjectsFromArray:tags]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + 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 setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + if (!data) return @[]; + + NSError *jsonError; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) return @[]; + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) return @[]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *bookmark in results) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark { + NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; + NSString *url = [bookmark objectForKey:@"url"]; + NSString *title = [bookmark objectForKey:@"title"]; + NSString *description = [bookmark objectForKey:@"description"]; + + if (!bookmarkId || !url) return nil; + + QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]]; + [newObject setObject:url forType:QSURLType]; + [newObject setName:title.length > 0 ? title : url]; + [newObject setDetails:description.length > 0 ? description : @""]; + [newObject setPrimaryType:QSURLType]; + + return newObject; +} + +@end diff --git a/REFACTOR_GUIDE.md b/REFACTOR_GUIDE.md new file mode 100644 index 0000000..2176a98 --- /dev/null +++ b/REFACTOR_GUIDE.md @@ -0,0 +1,118 @@ +# 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 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 +@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/SocialSite.h new file mode 100644 index 0000000..f2c848c --- /dev/null +++ b/SocialSite.h @@ -0,0 +1,23 @@ +// +// SocialSite.h +// QSDeliciousPlugIn +// +// Social bookmark site enumeration +// + +#import + +typedef NS_ENUM(NSInteger, SocialSite) { + SocialSiteDelicious = 0, + SocialSiteMagnolia = 1, + SocialSitePinboard = 2, + SocialSiteLinkding = 3 +}; + +@interface SocialSiteHelper : NSObject + ++ (NSString *)displayNameForSite:(SocialSite)site; ++ (NSString *)siteURLForSite:(SocialSite)site; ++ (NSString *)reversedSiteURLForSite:(SocialSite)site; + +@end \ No newline at end of file diff --git a/SocialSite.m b/SocialSite.m new file mode 100644 index 0000000..d002235 --- /dev/null +++ b/SocialSite.m @@ -0,0 +1,55 @@ +// +// SocialSite.m +// QSDeliciousPlugIn +// + +#import "SocialSite.h" + +@implementation SocialSiteHelper + ++ (NSString *)displayNameForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"Pinboard"; + case SocialSiteLinkding: + return @"Linkding"; + default: + return @"Unknown"; + } +} + ++ (NSString *)siteURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"pinboard.in"; + case SocialSiteLinkding: + return @""; // Will be provided by user as host + default: + return nil; + } +} + ++ (NSString *)reversedSiteURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"us.icio.del"; + case SocialSiteMagnolia: + return @"com.gnolia.ma"; + case SocialSitePinboard: + return @"in.pinboard"; + case SocialSiteLinkding: + return @"linkding"; // Generic identifier + default: + return nil; + } +} + +@end \ No newline at end of file diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index ad177ea..c117aca 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -1 +1,18 @@ -"QSDeliciousPrefPane" = "del.icio.us"; +"QSDeliciousPrefPane" = "Social Bookmarks"; + +/* Site Names */ +"SiteDelicious" = "del.icio.us"; +"SiteMagnolia" = "ma.gnolia.com"; +"SitePinboard" = "Pinboard"; +"SiteLinkding" = "Linkding"; + +/* UI Labels */ +"Username" = "Username"; +"Password" = "Password"; +"Host" = "Host"; +"IncludeTags" = "Include Tags"; +"Site" = "Bookmark Service"; + +/* Help Text */ +"LinkdingHostHelp" = "For Linkding, enter your server URL (e.g., https://bookmarks.example.com)"; +"LinkdingPasswordHelp" = "For Linkding, use your API Token as the password"; From a081f7ea92f32b8284cce4d11f8e899b08754acf Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 7 Sep 2025 11:38:39 +0200 Subject: [PATCH 02/11] Save the files when fetching the objects --- QSDeliciousPlugIn_Source.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 43a32cb..ab8c1f1 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -205,6 +205,9 @@ - (void)setCurrentPassword:(NSString *)newPassword { - (NSArray *)objectsForEntry:(NSDictionary *)theEntry { NSLog(@"WE HAVE BEEN REQUESTED"); + + [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:theEntry]; + SocialSite site = [self siteIndex]; NSString *username = [self currentUsername]; NSString *password = [self currentPassword]; From 0f26c5c3b3e606c89724067d4785323de3fee751 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 7 Sep 2025 13:14:48 +0200 Subject: [PATCH 03/11] Autosave on change --- QSDeliciousPlugIn_Source.h | 2 ++ QSDeliciousPlugIn_Source.m | 11 +++++------ QSDeliciousPlugIn_Source.xib | 5 +++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/QSDeliciousPlugIn_Source.h b/QSDeliciousPlugIn_Source.h index 9d22d91..1eff1c7 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -17,6 +17,8 @@ IBOutlet NSTextField *passField; IBOutlet NSTextField *hostField; } + +- (IBAction)settingsChanged:(id)sender; @end @interface QSCatalogEntry (OldStyleSourceSupport) diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index ab8c1f1..fb09a97 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -12,10 +12,6 @@ @implementation QSDeliciousPlugIn_Source -+ (void)initialize { - [self setKeys:[NSArray arrayWithObject:@"selection"] triggerChangeNotificationsForDependentKey:@"currentPassword"]; -} - - (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } @@ -59,6 +55,11 @@ - (BOOL)includeTags { return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] boolValue]; } + +- (IBAction)settingsChanged:(id)sender { + [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:self.selectedEntry]; +} + #pragma mark - Keychain Access - (SecProtocolType)protocolTypeForString:(NSString *)protocol { @@ -206,8 +207,6 @@ - (void)setCurrentPassword:(NSString *)newPassword { - (NSArray *)objectsForEntry:(NSDictionary *)theEntry { NSLog(@"WE HAVE BEEN REQUESTED"); - [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:theEntry]; - SocialSite site = [self siteIndex]; NSString *username = [self currentUsername]; NSString *password = [self currentPassword]; diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib index 65d8dda..83b620f 100644 --- a/QSDeliciousPlugIn_Source.xib +++ b/QSDeliciousPlugIn_Source.xib @@ -88,6 +88,7 @@ + @@ -107,6 +108,7 @@ + @@ -122,6 +124,7 @@ + @@ -140,6 +143,7 @@ + @@ -155,6 +159,7 @@ + From a5e25cc05e5e33e114331cc826211c89adbcfa6a Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 10:38:39 +0200 Subject: [PATCH 04/11] Reorganize code, WIP save for tag handling --- .../QSBookmarkProviderFactory.h | 0 .../QSBookmarkProviderFactory.m | 0 .../QSBookmarkProvider.h | 4 +- .../QSDeliciousAPIProvider.h | 0 .../QSDeliciousAPIProvider.m | 7 +- .../QSLinkdingProvider.h | 0 .../QSLinkdingProvider.m | 11 +- QSDeliciousPlugIn.xcodeproj/project.pbxproj | 52 ++- QSDeliciousPlugInTests.m | 89 ----- QSDeliciousPlugIn_Source.h | 1 + QSDeliciousPlugIn_Source.m | 317 +++++++++--------- QSDeliciousPlugIn_Source.xib | 2 +- REFACTOR_GUIDE.md | 118 ------- SocialSite.h => Types/SocialSite.h | 0 SocialSite.m => Types/SocialSite.m | 0 15 files changed, 222 insertions(+), 379 deletions(-) rename QSBookmarkProviderFactory.h => Factories/QSBookmarkProviderFactory.h (100%) rename QSBookmarkProviderFactory.m => Factories/QSBookmarkProviderFactory.m (100%) rename QSBookmarkProvider.h => Protocols/QSBookmarkProvider.h (88%) rename QSDeliciousAPIProvider.h => Providers/QSDeliciousAPIProvider.h (100%) rename QSDeliciousAPIProvider.m => Providers/QSDeliciousAPIProvider.m (94%) rename QSLinkdingProvider.h => Providers/QSLinkdingProvider.h (100%) rename QSLinkdingProvider.m => Providers/QSLinkdingProvider.m (93%) delete mode 100644 QSDeliciousPlugInTests.m delete mode 100644 REFACTOR_GUIDE.md rename SocialSite.h => Types/SocialSite.h (100%) rename SocialSite.m => Types/SocialSite.m (100%) diff --git a/QSBookmarkProviderFactory.h b/Factories/QSBookmarkProviderFactory.h similarity index 100% rename from QSBookmarkProviderFactory.h rename to Factories/QSBookmarkProviderFactory.h diff --git a/QSBookmarkProviderFactory.m b/Factories/QSBookmarkProviderFactory.m similarity index 100% rename from QSBookmarkProviderFactory.m rename to Factories/QSBookmarkProviderFactory.m diff --git a/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h similarity index 88% rename from QSBookmarkProvider.h rename to 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 similarity index 100% rename from QSDeliciousAPIProvider.h rename to Providers/QSDeliciousAPIProvider.h diff --git a/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m similarity index 94% rename from QSDeliciousAPIProvider.m rename to Providers/QSDeliciousAPIProvider.m index d97d1d2..ab09f88 100644 --- a/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -70,7 +70,7 @@ - (NSString *)tagURLType { 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 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam 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 similarity index 100% rename from QSLinkdingProvider.h rename to Providers/QSLinkdingProvider.h diff --git a/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m similarity index 93% rename from QSLinkdingProvider.m rename to Providers/QSLinkdingProvider.m index f97f5c7..094f318 100644 --- a/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -42,7 +42,7 @@ - (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSSt [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 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam return @[]; } - NSLog(@"WE ARE ABOUT TO REQUEST TO URL: %@", requestURL); - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; @@ -132,8 +130,11 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam 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 = ""; }; B5CF7D632E6B73D1008A0EE6 /* QSBookmarkProviderFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProviderFactory.h; sourceTree = ""; }; B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSBookmarkProviderFactory.m; sourceTree = ""; }; - B5CF7D672E6B7457008A0EE6 /* QSDeliciousPlugInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QSDeliciousPlugInTests.m; sourceTree = ""; }; - B5CF7D682E6B746E008A0EE6 /* REFACTOR_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTOR_GUIDE.md; sourceTree = ""; }; B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QSDeliciousPlugIn_Source.xib; sourceTree = ""; }; D475F9AF18B2992D0012243C /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; D475F9B018B2992D0012243C /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -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 = ""; + }; + B59368032E6EC63700DBD0F1 /* Providers */ = { + isa = PBXGroup; + children = ( B5CF7D5F2E6B7386008A0EE6 /* QSDeliciousAPIProvider.h */, B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */, B5CF7D612E6B73A5008A0EE6 /* QSLinkdingProvider.h */, B5CF7D622E6B73C9008A0EE6 /* QSLinkdingProvider.m */, + ); + path = Providers; + sourceTree = ""; + }; + B59368042E6EC65000DBD0F1 /* Types */ = { + isa = PBXGroup; + children = ( + B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, + B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, + ); + path = Types; + sourceTree = ""; + }; + B59368052E6EC66500DBD0F1 /* Protocols */ = { + isa = PBXGroup; + children = ( + B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */, + ); + path = Protocols; + sourceTree = ""; + }; + 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 = ""; }; 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 -#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 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 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 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 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 #import +#import @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 *)currentHost { } - (NSString *)currentPassword { - return [self.selectedEntry.sourceSettings objectForKey:@"password"]; + return self.internalPassword; } - (BOOL)includeTags { @@ -56,174 +68,159 @@ - (BOOL)includeTags { } +// 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; -} -- (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; -} +#pragma mark - Keychain Helper Methods -- (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; +- (NSString *)keychainKeyForIdentifier:(NSString *)identifier { + return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; } -- (NSString *)keychainPasswordForURL:(NSURL *)url { - return [self passwordForHost:[url host] user:[url user] andScheme:[url scheme]]; +- (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; } -- (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], +- (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 err; + + return status; } -- (NSString *)oldCurrentPassword { - NSString *account = [self currentUsername]; - if (!account) return nil; - - SocialSite site = [self siteIndex]; - NSString *host = nil; +- (OSStatus)deletePasswordFromKeychainForKey:(NSString *)key { + const char *service = "QSSocialBookmarks"; + const char *account = [key UTF8String]; - // 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]; + 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; } - - NSURL *keychainURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@@%@/", account, host]]; - NSString *password = [self keychainPasswordForURL:keychainURL]; - - return password; + + return findStatus; } -- (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 Keychain Methods + +- (void)loadPasswordFromKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier) { + self.internalPassword = nil; + return; + } - // 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]; + NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + [self setPassword: [self passwordFromKeychainForKey:keychainKey]]; +} + +- (void)savePasswordToKeychain { + if (!self.selectedEntry || !self.selectedEntry.identifier || !self.internalPassword) { + return; + } + + 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, newPassword, host]]; - - [self addURLPasswordToKeychain:keychainURL]; +} + +#pragma mark - Password Property Accessors + +- (NSString *)password { + return self.internalPassword; +} + +- (void)setPassword:(NSString *)password { + self.internalPassword = password; + + // 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 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 @@ - (void)setQuickIconForObject:(QSObject *)object { [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 @@ - (BOOL)loadChildrenForObject:(QSObject *)object { 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 @@ - + 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 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 -@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 similarity index 100% rename from SocialSite.h rename to Types/SocialSite.h diff --git a/SocialSite.m b/Types/SocialSite.m similarity index 100% rename from SocialSite.m rename to Types/SocialSite.m From cec89d9e844dc39b84778e1b88e74e23f9ca2e6a Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 16:04:35 +0200 Subject: [PATCH 05/11] WIP generic Delicious API compatible --- Factories/QSBookmarkProviderFactory.m | 10 +-- Info.plist | 2 +- Protocols/QSBookmarkProvider.h | 15 ---- Providers/QSDeliciousAPIProvider.h | 5 +- Providers/QSDeliciousAPIProvider.m | 93 +++++++++++---------- Providers/QSLinkdingProvider.m | 25 ++---- QSDeliciousPlugIn.xcodeproj/project.pbxproj | 6 ++ QSDeliciousPlugIn_Source.m | 59 +++++++------ QSDeliciousPlugIn_Source.xib | 64 +++++++------- Types/Constants.h | 9 ++ Types/Constants.m | 8 ++ Types/SocialSite.h | 7 +- Types/SocialSite.m | 22 +++-- 13 files changed, 167 insertions(+), 158 deletions(-) create mode 100644 Types/Constants.h create mode 100644 Types/Constants.m diff --git a/Factories/QSBookmarkProviderFactory.m b/Factories/QSBookmarkProviderFactory.m index 6fcfdfb..75fd0e2 100644 --- a/Factories/QSBookmarkProviderFactory.m +++ b/Factories/QSBookmarkProviderFactory.m @@ -34,17 +34,13 @@ - (instancetype)init { - (void)setupProviders { NSMutableArray *mutableProviders = [NSMutableArray array]; - // Create Pinboard API providers for each supported site - QSDeliciousAPIProvider *deliciousProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteDelicious]; - QSDeliciousAPIProvider *magnoliaProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSiteMagnolia]; - QSDeliciousAPIProvider *pinboardProvider = [[QSDeliciousAPIProvider alloc] initWithSite:SocialSitePinboard]; + // Create Delicious / Pinboard API providers for each supported site + QSDeliciousAPIProvider *deliciousAPIProvider = [[QSDeliciousAPIProvider alloc] init]; // Create Linkding provider QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; - [mutableProviders addObject:deliciousProvider]; - [mutableProviders addObject:magnoliaProvider]; - [mutableProviders addObject:pinboardProvider]; + [mutableProviders addObject:deliciousAPIProvider]; [mutableProviders addObject:linkdingProvider]; self.providers = [mutableProviders copy]; diff --git a/Info.plist b/Info.plist index b916b8c..5101e87 100644 --- a/Info.plist +++ b/Info.plist @@ -42,7 +42,7 @@ QSObjectHandlers - us.icio.del.tag + qs.tag.socialbookmark QSDeliciousPlugIn_Source QSObjectSources diff --git a/Protocols/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h index 136ea2d..9131e3c 100644 --- a/Protocols/QSBookmarkProvider.h +++ b/Protocols/QSBookmarkProvider.h @@ -24,25 +24,10 @@ */ - (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 - */ -- (SocialSite)supportedSite; - -/** - * Get display name for this provider - */ -- (NSString *)providerName; - @optional /** * Get bookmarks for a specific tag (used for child loading) */ - (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; -/** - * Get the tag URL type identifier for this provider - */ -- (NSString *)tagURLType; - @end diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h index 0064264..08a0534 100644 --- a/Providers/QSDeliciousAPIProvider.h +++ b/Providers/QSDeliciousAPIProvider.h @@ -12,12 +12,9 @@ @interface QSDeliciousAPIProvider : NSObject @property (nonatomic, strong) NSMutableArray *posts; -@property (nonatomic, assign) SocialSite site; - -- (instancetype)initWithSite:(SocialSite)site; // Subclasses can override these methods -- (NSString *)apiURLForSite:(SocialSite)site; +- (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host; - (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; - (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; - (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m index ab09f88..31b283d 100644 --- a/Providers/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -5,52 +5,59 @@ #import "QSDeliciousAPIProvider.h" #import "SocialSite.h" +#import "Constants.h" #import @implementation QSDeliciousAPIProvider -- (instancetype)initWithSite:(SocialSite)site { - self = [super init]; - if (self) { - _site = site; - _posts = [NSMutableArray array]; - } - return self; -} - - (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == self.site) && - username.length > 0 && + return ( + site == SocialSiteDelicious || + site == SocialSiteMagnolia || + site == SocialSitePinboard || + site == SocialSiteSelfHostedDeliciousCompatible + ) && + username.length > 0 && password.length > 0 && - (site != SocialSiteLinkding); // This provider doesn't handle Linkding -} - -- (SocialSite)supportedSite { - return self.site; + (site != SocialSiteSelfHostedDeliciousCompatible || host.length > 0); } -- (NSString *)providerName { - return [SocialSiteHelper displayNameForSite:self.site]; -} - -- (NSString *)apiURLForSite:(SocialSite)site { +- (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host { switch (site) { case SocialSiteDelicious: return @"api.del.icio.us/v1"; case SocialSiteMagnolia: return @"ma.gnolia.com/api/mirrord/v1"; case SocialSitePinboard: - return @"api.pinboard.in/v1"; + return @"https://api.pinboard.in/v1"; + case SocialSiteSelfHostedDeliciousCompatible: + return [NSString stringWithFormat:@"%@/v1", host]; default: return nil; } } +- (BOOL)usesAuthToken:(SocialSite)site { + switch (site) { + case SocialSitePinboard: + case SocialSiteSelfHostedDeliciousCompatible: + return YES; + default: + return NO; + } +} - (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSString *apiURL = [self apiURLForSite:site]; + NSString *apiURL = [self apiURLForSite:site andHost: host]; if (!apiURL) return nil; - - NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + + NSString *urlString; + if ([self usesAuthToken:site]) { + // Pinboard and pinboard compatible sites require an + // auth token rather than a password. + urlString = [NSString stringWithFormat:@"%@/posts/all?auth_token=%@:%@", apiURL, username, password]; + } else { + urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + } return [NSURL URLWithString:urlString]; } @@ -61,15 +68,12 @@ - (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)user } - (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { - NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *safeURL = [[siteURL componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", safeURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; [data writeToFile:cachePath atomically:NO]; } -- (NSString *)tagURLType { - return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; -} - - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { // Try cached data first @@ -79,7 +83,7 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam if (![data length]) { NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; if (!requestURL) return @[]; - + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; @@ -87,7 +91,7 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - NSError *error; + NSError *error = nil; data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; if (error) { @@ -98,6 +102,8 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam // Cache the data [self cacheBookmarkData:data forSite:site username:username]; } + + NSLog(@"WE ARE READY TO PARSE"); // Parse XML data NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; @@ -105,6 +111,8 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam self.posts = [NSMutableArray arrayWithCapacity:1]; [postParser parse]; + + NSLog(@"STILL READY: %ld", [self.posts count]); NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; NSMutableSet *tagSet = [NSMutableSet set]; @@ -129,15 +137,16 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam if (includeTags) { for (NSString *tag in tagSet) { if (tag.length > 0) { - QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]]; - [tagObject setObject:tag forType:[self tagURLType]]; - [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]; + QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [SocialSiteHelper displayNameForSite:site], tag]]; + + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; + [tagObject setObject:identifier forMeta:kTagIdentifierField]; + [tagObject setName:tag]; + [tagObject setPrimaryType:kTagType]; + [objects addObject:tagObject]; } } } diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m index 094f318..c7bf33f 100644 --- a/Providers/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -5,6 +5,7 @@ #import "QSLinkdingProvider.h" #import "SocialSite.h" +#import "Constants.h" #import @implementation QSLinkdingProvider @@ -16,18 +17,6 @@ - (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(N host.length > 0; } -- (SocialSite)supportedSite { - return SocialSiteLinkding; -} - -- (NSString *)providerName { - return @"Linkding"; -} - -- (NSString *)tagURLType { - return @"tag.linkding"; -} - - (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { // Create a safe filename from host NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; @@ -129,14 +118,14 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam for (NSString *tag in tagSet) { if (tag.length > 0) { QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; - [tagObject setObject:tag forType:[self tagURLType]]; - [tagObject setObject:@(site) forMeta:@"source.site"]; - [tagObject setObject:username forMeta:@"source.username"]; - [tagObject setObject:host forMeta:@"source.host"]; + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; // We need the identifier to be able to fetch the keychain password - [tagObject setObject:identifier forMeta:@"source.identifier"]; + [tagObject setObject:identifier forMeta:kTagIdentifierField]; [tagObject setName:tag]; - [tagObject setPrimaryType:[self tagURLType]]; + [tagObject setPrimaryType:kTagType]; [objects addObject:tagObject]; } } diff --git a/QSDeliciousPlugIn.xcodeproj/project.pbxproj b/QSDeliciousPlugIn.xcodeproj/project.pbxproj index d68169d..803ae6e 100644 --- a/QSDeliciousPlugIn.xcodeproj/project.pbxproj +++ b/QSDeliciousPlugIn.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 7FB0DC2F0B91FFC600A5B6FF /* QSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2D0B91FFC500A5B6FF /* QSCore.framework */; }; 7FB0DC300B91FFC600A5B6FF /* QSFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */; }; 8D1AC9700486D14A00FE50C9 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD92D38A0106425D02CA0E72 /* Cocoa.framework */; }; + B59368092E6F04E200DBD0F1 /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = B59368082E6F04DF00DBD0F1 /* Constants.m */; }; B5CF7D6A2E6B7631008A0EE6 /* QSDeliciousPlugIn_Source.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5CF7D692E6B74FE008A0EE6 /* QSDeliciousPlugIn_Source.xib */; }; B5CF7D6E2E6B7A53008A0EE6 /* QSDeliciousAPIProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D602E6B73A0008A0EE6 /* QSDeliciousAPIProvider.m */; }; B5CF7D6F2E6B7A53008A0EE6 /* QSBookmarkProviderFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = B5CF7D642E6B73DA008A0EE6 /* QSBookmarkProviderFactory.m */; }; @@ -33,6 +34,8 @@ 7FB0DC2E0B91FFC600A5B6FF /* QSFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QSFoundation.framework; path = /Applications/Quicksilver.app/Contents/Frameworks/QSFoundation.framework; sourceTree = ""; }; 8D1AC9730486D14A00FE50C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8D1AC9740486D14A00FE50C9 /* Social Bookmarks Plugin.qsplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Social Bookmarks Plugin.qsplugin"; sourceTree = BUILT_PRODUCTS_DIR; }; + B59368072E6F021E00DBD0F1 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + B59368082E6F04DF00DBD0F1 /* Constants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = ""; }; B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocialSite.h; sourceTree = ""; }; B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocialSite.m; sourceTree = ""; }; B5CF7D5E2E6B737E008A0EE6 /* QSBookmarkProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QSBookmarkProvider.h; sourceTree = ""; }; @@ -153,6 +156,8 @@ B59368042E6EC65000DBD0F1 /* Types */ = { isa = PBXGroup; children = ( + B59368072E6F021E00DBD0F1 /* Constants.h */, + B59368082E6F04DF00DBD0F1 /* Constants.m */, B5CF7D5C2E6B7370008A0EE6 /* SocialSite.h */, B5CF7D5D2E6B7377008A0EE6 /* SocialSite.m */, ); @@ -288,6 +293,7 @@ B5CF7D702E6B7A53008A0EE6 /* QSLinkdingProvider.m in Sources */, B5CF7D712E6B7A53008A0EE6 /* SocialSite.m in Sources */, E182BCD406FC8203007BF2C2 /* QSDeliciousPlugIn_Source.m in Sources */, + B59368092E6F04E200DBD0F1 /* Constants.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 55eedf0..4d841a1 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -7,9 +7,9 @@ // #import "QSDeliciousPlugIn_Source.h" +#import "Constants.h" #import #import -#import @implementation QSDeliciousPlugIn_Source @@ -72,6 +72,8 @@ - (BOOL)includeTags { // to force the catalog to save the current values. - (IBAction)settingsChanged:(id)sender { [[NSNotificationCenter defaultCenter] postNotificationName:QSCatalogEntryChangedNotification object:self.selectedEntry]; + [self willChangeValueForKey:@"isHostVisible"]; + [self didChangeValueForKey:@"isHostVisible"]; } @@ -198,6 +200,12 @@ - (void)setPassword:(NSString *)password { }); } +#pragma mark - Host Visibility Control +- (BOOL) isHostVisible { + return [SocialSiteHelper hasVariableHost:[self siteIndex]]; +} +- (void)setIsHostVisible:(BOOL)isVisible { } + #pragma mark - Objects For Entry - (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { @@ -219,23 +227,22 @@ - (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { NSLog(@"No provider available for site %ld with username %@", (long)site, username); return @[]; } - + return [provider fetchBookmarksForSite:site username:username password:password identifier:identifier host:host includeTags:includeTags]; } -- (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { - SocialSite site = [self siteIndex]; - NSString *password = [self currentPassword]; - NSString *host = [self currentHost]; - - // Get the appropriate provider using the factory - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - id provider = [factory providerForSite:site username:username password:password host:host]; +- (NSArray *)objectsForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username identifier:(NSString *)identifier host:(NSString *)host { + + NSString *keychainKey = [self keychainKeyForIdentifier:identifier]; + NSString *password = [self passwordFromKeychainForKey:keychainKey]; + + QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; + id 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 @[]; - } + if (!provider) { + NSLog(@"No provider available for site %ld with username %@", (long)site, username); + return @[]; + } // Check if provider supports tag-based fetching if ([provider respondsToSelector:@selector(fetchBookmarksForTag:site:username:password:host:)]) { @@ -248,9 +255,17 @@ - (NSArray *)objectsForTag:(NSString *)tag username:(NSString *)username { #pragma mark - Object Handler Methods - (void)setQuickIconForObject:(QSObject *)object { - [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; + if (@available(macOS 11.0, *)) { + NSImage *image = [NSImage imageWithSystemSymbolName:@"tag" accessibilityDescription:@"Bookmark"]; + [object setIcon:image]; + } else { + [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; + } } +// All our objects will have children. URLs will have tags, and tags will have URLs. +- (BOOL)objectHasChildren:(QSObject *) object { return YES; } + // 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. @@ -281,21 +296,13 @@ - (BOOL)loadChildrenForObject:(QSObject *)object { return NO; } - NSString *tagType = nil; - if (site == SocialSiteLinkding) { - tagType = @"tag.linkding"; - } else { - NSString *reversedURL = [SocialSiteHelper reversedSiteURLForSite:site]; - tagType = [NSString stringWithFormat:@"tag.%@", reversedURL]; - } - - NSString *tag = [object objectForType:tagType]; + NSString *tag = [object objectForType:kTagType]; if (!tag) { NSLog(@"We could not find a valid tag type."); return NO; } - - NSArray *children = [self objectsForTag:tag username:username]; + + NSArray *children = [self objectsForTag:tag site:site username:username identifier:identifier host:host]; [object setChildren:children]; return YES; } diff --git a/QSDeliciousPlugIn_Source.xib b/QSDeliciousPlugIn_Source.xib index c688346..5f56ddc 100644 --- a/QSDeliciousPlugIn_Source.xib +++ b/QSDeliciousPlugIn_Source.xib @@ -33,7 +33,7 @@ - + @@ -45,7 +45,7 @@ - + @@ -57,7 +57,7 @@ - + @@ -67,31 +67,14 @@ - - - - - - - - - - - + @@ -103,7 +86,8 @@ - + + @@ -113,7 +97,7 @@ - + @@ -129,7 +113,7 @@ - + @@ -148,7 +132,7 @@ - + @@ -160,9 +144,29 @@ + + + NSNegateBoolean + + + diff --git a/Types/Constants.h b/Types/Constants.h new file mode 100644 index 0000000..75dd240 --- /dev/null +++ b/Types/Constants.h @@ -0,0 +1,9 @@ + +#pragma mark - Tag QSObject Constants + +extern NSString *const kTagType; + +extern NSString *const kTagSiteField; +extern NSString *const kTagUsernameField; +extern NSString *const kTagHostField; +extern NSString *const kTagIdentifierField; diff --git a/Types/Constants.m b/Types/Constants.m new file mode 100644 index 0000000..9300b54 --- /dev/null +++ b/Types/Constants.m @@ -0,0 +1,8 @@ +#pragma mark - Tag QSObject Constants + +NSString *const kTagType = @"qs.tag.socialbookmark"; + +NSString *const kTagSiteField = @"source.site"; +NSString *const kTagUsernameField = @"source.username"; +NSString *const kTagHostField = @"source.host"; +NSString *const kTagIdentifierField = @"source.identifier"; diff --git a/Types/SocialSite.h b/Types/SocialSite.h index f2c848c..130bc40 100644 --- a/Types/SocialSite.h +++ b/Types/SocialSite.h @@ -11,13 +11,14 @@ typedef NS_ENUM(NSInteger, SocialSite) { SocialSiteDelicious = 0, SocialSiteMagnolia = 1, SocialSitePinboard = 2, - SocialSiteLinkding = 3 + SocialSiteLinkding = 3, + SocialSiteSelfHostedDeliciousCompatible = 4 }; @interface SocialSiteHelper : NSObject + (NSString *)displayNameForSite:(SocialSite)site; + (NSString *)siteURLForSite:(SocialSite)site; -+ (NSString *)reversedSiteURLForSite:(SocialSite)site; ++ (BOOL)hasVariableHost:(SocialSite)site; -@end \ No newline at end of file +@end diff --git a/Types/SocialSite.m b/Types/SocialSite.m index d002235..b73533f 100644 --- a/Types/SocialSite.m +++ b/Types/SocialSite.m @@ -17,6 +17,8 @@ + (NSString *)displayNameForSite:(SocialSite)site { return @"Pinboard"; case SocialSiteLinkding: return @"Linkding"; + case SocialSiteSelfHostedDeliciousCompatible: + return @"Self-Hosted (Delicious Compatible)"; default: return @"Unknown"; } @@ -30,6 +32,7 @@ + (NSString *)siteURLForSite:(SocialSite)site { return @"ma.gnolia.com"; case SocialSitePinboard: return @"pinboard.in"; + case SocialSiteSelfHostedDeliciousCompatible: case SocialSiteLinkding: return @""; // Will be provided by user as host default: @@ -37,19 +40,14 @@ + (NSString *)siteURLForSite:(SocialSite)site { } } -+ (NSString *)reversedSiteURLForSite:(SocialSite)site { ++ (BOOL)hasVariableHost:(SocialSite)site { switch (site) { - case SocialSiteDelicious: - return @"us.icio.del"; - case SocialSiteMagnolia: - return @"com.gnolia.ma"; - case SocialSitePinboard: - return @"in.pinboard"; - case SocialSiteLinkding: - return @"linkding"; // Generic identifier - default: - return nil; + case SocialSiteSelfHostedDeliciousCompatible: + case SocialSiteLinkding: + return YES; + default: + return NO; } } -@end \ No newline at end of file +@end From 848982f156a86038438918e3a51cc0f04be9c78c Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 21:48:56 +0200 Subject: [PATCH 06/11] Fix the pinboard/self-hosted delicious API calls --- Providers/QSDeliciousAPIProvider.h | 4 +-- Providers/QSDeliciousAPIProvider.m | 41 ++++++++++++++++-------------- README.markdown | 12 ++++++++- Types/SocialSite.h | 2 +- Types/SocialSite.m | 6 +++-- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h index 08a0534..7979812 100644 --- a/Providers/QSDeliciousAPIProvider.h +++ b/Providers/QSDeliciousAPIProvider.h @@ -16,7 +16,7 @@ // Subclasses can override these methods - (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host; - (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host; +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username host:(NSString *)host; @end diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m index 31b283d..0c37a43 100644 --- a/Providers/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -47,38 +47,45 @@ - (BOOL)usesAuthToken:(SocialSite)site { } - (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSString *apiURL = [self apiURLForSite:site andHost: host]; + NSString *apiURL = [self apiURLForSite:site andHost: host]; if (!apiURL) return nil; NSString *urlString; if ([self usesAuthToken:site]) { // Pinboard and pinboard compatible sites require an // auth token rather than a password. - urlString = [NSString stringWithFormat:@"%@/posts/all?auth_token=%@:%@", apiURL, username, password]; + urlString = [NSString stringWithFormat:@"%@/posts/all?auth_token=%@", apiURL, password]; } else { urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; } return [NSURL URLWithString:urlString]; } -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username { - NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; - return [NSData dataWithContentsOfFile:cachePath]; +# pragma mark - Cache + +- (NSString *)cachePathForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host create:(BOOL) create { + + NSString *siteURL = [SocialSiteHelper cacheKeyForSite:site]; + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + return [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], create) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.xml", safeHost, username]]; +} + + +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host { + NSString *cachePath = [self cachePathForSite: site username: username host: host create: NO]; + return [NSData dataWithContentsOfFile:cachePath]; } -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { - NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; - NSString *safeURL = [[siteURL componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", safeURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username host:(NSString *)host { + NSString *cachePath = [self cachePathForSite: site username: username host: host create: YES]; [data writeToFile:cachePath atomically:NO]; } - (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]; - + NSData *data = [self cachedBookmarkDataForSite:site username:username host:host]; + // If no cached data, fetch from API if (![data length]) { NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; @@ -87,7 +94,7 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"POST"]; + [theRequest setHTTPMethod:@"GET"]; [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; @@ -100,11 +107,9 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam } // Cache the data - [self cacheBookmarkData:data forSite:site username:username]; + [self cacheBookmarkData:data forSite:site username:username host:host]; } - NSLog(@"WE ARE READY TO PARSE"); - // Parse XML data NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; [postParser setDelegate:self]; @@ -112,8 +117,6 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam self.posts = [NSMutableArray arrayWithCapacity:1]; [postParser parse]; - NSLog(@"STILL READY: %ld", [self.posts count]); - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; NSMutableSet *tagSet = [NSMutableSet set]; @@ -155,7 +158,7 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)usernam } - (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSData *data = [self cachedBookmarkDataForSite:site username:username]; + NSData *data = [self cachedBookmarkDataForSite:site username:username host:host]; if (!data) return @[]; NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; diff --git a/README.markdown b/README.markdown index c22d609..5a08e8e 100644 --- a/README.markdown +++ b/README.markdown @@ -58,4 +58,14 @@ By downloading and/or using this software you agree to the following terms of us limitations under the License. -Which basically means: whatever you do, I can't be held accountable if something breaks. \ No newline at end of file +Which basically means: whatever you do, I can't be held accountable if something breaks. + +Supported Services +----------- + +This plugin supports Delicious compatible services (eg. Linkhut), and Linkding. It's important to note that delicious compatible systems aren't all compatible in the same way, so you might need to make some adjustments: + + +- For every self-hsoted service, make sure to include the protocol (eg. https://). +- For delicious compatible items, include everything before `/v1`. So if it's `https://api.ln.ht/v1` you just include `https://api.ln.ht`. But if it's `https://myservice.com/api/v1` then you should include `https://myservice.com/api` with no trailing slash. +- For delicious compatible items, check what they expect for the `auth_token` field and use that as your password. For example, in pinboard it's `username:token`, while in linkhut it's just `token`. diff --git a/Types/SocialSite.h b/Types/SocialSite.h index 130bc40..43155c7 100644 --- a/Types/SocialSite.h +++ b/Types/SocialSite.h @@ -18,7 +18,7 @@ typedef NS_ENUM(NSInteger, SocialSite) { @interface SocialSiteHelper : NSObject + (NSString *)displayNameForSite:(SocialSite)site; -+ (NSString *)siteURLForSite:(SocialSite)site; ++ (NSString *)cacheKeyForSite:(SocialSite)site; + (BOOL)hasVariableHost:(SocialSite)site; @end diff --git a/Types/SocialSite.m b/Types/SocialSite.m index b73533f..369f86d 100644 --- a/Types/SocialSite.m +++ b/Types/SocialSite.m @@ -24,7 +24,8 @@ + (NSString *)displayNameForSite:(SocialSite)site { } } -+ (NSString *)siteURLForSite:(SocialSite)site { +// This is used for caching key ++ (NSString *)cacheKeyForSite:(SocialSite)site { switch (site) { case SocialSiteDelicious: return @"del.icio.us"; @@ -33,8 +34,9 @@ + (NSString *)siteURLForSite:(SocialSite)site { case SocialSitePinboard: return @"pinboard.in"; case SocialSiteSelfHostedDeliciousCompatible: + return @"self-hosted-delicious"; case SocialSiteLinkding: - return @""; // Will be provided by user as host + return @"linkding"; default: return nil; } From 2d247ab64b84cf80d1beedb9521d7488bcb99f18 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 21:49:49 +0200 Subject: [PATCH 07/11] Apply formatting --- Factories/QSBookmarkProviderFactory.h | 10 +- Factories/QSBookmarkProviderFactory.m | 70 ++--- Protocols/QSBookmarkProvider.h | 20 +- Providers/QSDeliciousAPIProvider.h | 21 +- Providers/QSDeliciousAPIProvider.m | 364 +++++++++++++++----------- Providers/QSLinkdingProvider.h | 9 +- Providers/QSLinkdingProvider.m | 348 +++++++++++++----------- QSDeliciousPlugIn_Source.h | 8 +- QSDeliciousPlugIn_Source.m | 349 +++++++++++++----------- Types/SocialSite.h | 10 +- Types/SocialSite.m | 70 ++--- 11 files changed, 734 insertions(+), 545 deletions(-) diff --git a/Factories/QSBookmarkProviderFactory.h b/Factories/QSBookmarkProviderFactory.h index e2e694b..bc0ae79 100644 --- a/Factories/QSBookmarkProviderFactory.h +++ b/Factories/QSBookmarkProviderFactory.h @@ -5,20 +5,24 @@ // Factory for managing bookmark providers // -#import #import "QSBookmarkProvider.h" #import "SocialSite.h" +#import @interface QSBookmarkProviderFactory : NSObject -@property (nonatomic, strong, readonly) NSArray> *providers; +@property(nonatomic, strong, readonly) + NSArray> *providers; + (instancetype)sharedFactory; /** * Get the appropriate provider for the given site configuration */ -- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (id)providerForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host; /** * Get all available providers diff --git a/Factories/QSBookmarkProviderFactory.m b/Factories/QSBookmarkProviderFactory.m index 75fd0e2..56b4146 100644 --- a/Factories/QSBookmarkProviderFactory.m +++ b/Factories/QSBookmarkProviderFactory.m @@ -9,54 +9,62 @@ #import "SocialSite.h" @interface QSBookmarkProviderFactory () -@property (nonatomic, strong, readwrite) NSArray> *providers; +@property(nonatomic, strong, readwrite) + NSArray> *providers; @end @implementation QSBookmarkProviderFactory + (instancetype)sharedFactory { - static QSBookmarkProviderFactory *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; + static QSBookmarkProviderFactory *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; } - (instancetype)init { - self = [super init]; - if (self) { - [self setupProviders]; - } - return self; + self = [super init]; + if (self) { + [self setupProviders]; + } + return self; } - (void)setupProviders { - NSMutableArray *mutableProviders = [NSMutableArray array]; - - // Create Delicious / Pinboard API providers for each supported site - QSDeliciousAPIProvider *deliciousAPIProvider = [[QSDeliciousAPIProvider alloc] init]; - - // Create Linkding provider - QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; - - [mutableProviders addObject:deliciousAPIProvider]; - [mutableProviders addObject:linkdingProvider]; - - self.providers = [mutableProviders copy]; + NSMutableArray *mutableProviders = [NSMutableArray array]; + + // Create Delicious / Pinboard API providers for each supported site + QSDeliciousAPIProvider *deliciousAPIProvider = + [[QSDeliciousAPIProvider alloc] init]; + + // Create Linkding provider + QSLinkdingProvider *linkdingProvider = [[QSLinkdingProvider alloc] init]; + + [mutableProviders addObject:deliciousAPIProvider]; + [mutableProviders addObject:linkdingProvider]; + + self.providers = [mutableProviders copy]; } -- (id)providerForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - for (id provider in self.providers) { - if ([provider canHandleSite:site username:username password:password host:host]) { - return provider; - } +- (id)providerForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + for (id provider in self.providers) { + if ([provider canHandleSite:site + username:username + password:password + host:host]) { + return provider; } - return nil; + } + return nil; } - (NSArray> *)allProviders { - return self.providers; + return self.providers; } @end diff --git a/Protocols/QSBookmarkProvider.h b/Protocols/QSBookmarkProvider.h index 9131e3c..48e1417 100644 --- a/Protocols/QSBookmarkProvider.h +++ b/Protocols/QSBookmarkProvider.h @@ -5,8 +5,8 @@ // Protocol for social bookmark providers // -#import #import "SocialSite.h" +#import @class QSObject; @@ -16,18 +16,30 @@ /** * Check if this provider can handle the given site configuration */ -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (BOOL)canHandleSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host; /** * Fetch bookmarks for the given configuration * Returns an NSArray of QSObject instances */ -- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier 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; @optional /** * Get bookmarks for a specific tag (used for child loading) */ -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (NSArray *)fetchBookmarksForTag:(NSString *)tag + site:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host; @end diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h index 7979812..310b00b 100644 --- a/Providers/QSDeliciousAPIProvider.h +++ b/Providers/QSDeliciousAPIProvider.h @@ -6,17 +6,26 @@ // Used by Delicious, Magnolia, and Pinboard // -#import #import "QSBookmarkProvider.h" +#import -@interface QSDeliciousAPIProvider : NSObject +@interface QSDeliciousAPIProvider + : NSObject -@property (nonatomic, strong) NSMutableArray *posts; +@property(nonatomic, strong) NSMutableArray *posts; // Subclasses can override these methods - (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host; -- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host; -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username host:(NSString *)host; +- (NSURL *)requestURLForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host; +- (void)cacheBookmarkData:(NSData *)data + forSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host; @end diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m index 0c37a43..03991c1 100644 --- a/Providers/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -4,202 +4,264 @@ // #import "QSDeliciousAPIProvider.h" -#import "SocialSite.h" #import "Constants.h" +#import "SocialSite.h" #import @implementation QSDeliciousAPIProvider -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return ( - site == SocialSiteDelicious || - site == SocialSiteMagnolia || - site == SocialSitePinboard || - site == SocialSiteSelfHostedDeliciousCompatible - ) && - username.length > 0 && - password.length > 0 && - (site != SocialSiteSelfHostedDeliciousCompatible || host.length > 0); +- (BOOL)canHandleSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + return (site == SocialSiteDelicious || site == SocialSiteMagnolia || + site == SocialSitePinboard || + site == SocialSiteSelfHostedDeliciousCompatible) && + username.length > 0 && password.length > 0 && + (site != SocialSiteSelfHostedDeliciousCompatible || host.length > 0); } - (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host { - switch (site) { - case SocialSiteDelicious: - return @"api.del.icio.us/v1"; - case SocialSiteMagnolia: - return @"ma.gnolia.com/api/mirrord/v1"; - case SocialSitePinboard: - return @"https://api.pinboard.in/v1"; - case SocialSiteSelfHostedDeliciousCompatible: - return [NSString stringWithFormat:@"%@/v1", host]; - default: - return nil; - } + switch (site) { + case SocialSiteDelicious: + return @"api.del.icio.us/v1"; + case SocialSiteMagnolia: + return @"ma.gnolia.com/api/mirrord/v1"; + case SocialSitePinboard: + return @"https://api.pinboard.in/v1"; + case SocialSiteSelfHostedDeliciousCompatible: + return [NSString stringWithFormat:@"%@/v1", host]; + default: + return nil; + } } - (BOOL)usesAuthToken:(SocialSite)site { - switch (site) { - case SocialSitePinboard: - case SocialSiteSelfHostedDeliciousCompatible: - return YES; - default: - return NO; - } + switch (site) { + case SocialSitePinboard: + case SocialSiteSelfHostedDeliciousCompatible: + return YES; + default: + return NO; + } } -- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSString *apiURL = [self apiURLForSite:site andHost: host]; - if (!apiURL) return nil; - +- (NSURL *)requestURLForSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + NSString *apiURL = [self apiURLForSite:site andHost:host]; + if (!apiURL) + return nil; + NSString *urlString; if ([self usesAuthToken:site]) { // Pinboard and pinboard compatible sites require an // auth token rather than a password. - urlString = [NSString stringWithFormat:@"%@/posts/all?auth_token=%@", apiURL, password]; + urlString = [NSString + stringWithFormat:@"%@/posts/all?auth_token=%@", apiURL, password]; } else { - urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", + username, password, apiURL]; } - return [NSURL URLWithString:urlString]; + return [NSURL URLWithString:urlString]; } -# pragma mark - Cache +#pragma mark - Cache + +- (NSString *)cachePathForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host + create:(BOOL)create { -- (NSString *)cachePathForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host create:(BOOL) create { - NSString *siteURL = [SocialSiteHelper cacheKeyForSite:site]; - NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - return [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], create) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.xml", safeHost, username]]; + NSString *safeHost = + [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet + alphanumericCharacterSet] + invertedSet]] + componentsJoinedByString:@"-"]; + return [QSApplicationSupportSubPath( + [NSString stringWithFormat:@"Caches/%@/", siteURL], create) + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.xml", + safeHost, + username]]; } - -- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username host:(NSString *)host { - NSString *cachePath = [self cachePathForSite: site username: username host: host create: NO]; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host { + NSString *cachePath = [self cachePathForSite:site + username:username + host:host + create:NO]; return [NSData dataWithContentsOfFile:cachePath]; } -- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username host:(NSString *)host { - NSString *cachePath = [self cachePathForSite: site username: username host: host create: YES]; - [data writeToFile:cachePath atomically:NO]; +- (void)cacheBookmarkData:(NSData *)data + forSite:(SocialSite)site + username:(NSString *)username + host:(NSString *)host { + NSString *cachePath = [self cachePathForSite:site + username:username + host:host + create:YES]; + [data writeToFile:cachePath atomically:NO]; } -- (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 host:host]; - - // If no cached data, fetch from API - if (![data length]) { - NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; - if (!requestURL) return @[]; - - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"GET"]; - [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; - [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - - NSError *error = nil; - data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - if (error) { - NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); - return @[]; - } - - // Cache the data - [self cacheBookmarkData:data forSite:site username:username host:host]; +- (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 + host:host]; + + // If no cached data, fetch from API + if (![data length]) { + NSURL *requestURL = [self requestURLForSite:site + username:username + password:password + host:host]; + if (!requestURL) + return @[]; + + NSMutableURLRequest *theRequest = + [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"GET"]; + [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" + forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest + returningResponse:nil + error:&error]; + + if (error) { + NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); + return @[]; } - - // Parse XML data - NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; - [postParser setDelegate:self]; - - self.posts = [NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - NSMutableSet *tagSet = [NSMutableSet set]; - - // Create bookmark objects - for (NSDictionary *post in self.posts) { - QSObject *newObject = [self objectForPost:post]; - if (newObject) { - [objects addObject:newObject]; - - // Collect tags if requested - if (includeTags) { - NSString *tagString = [post objectForKey:@"tag"]; - if (tagString.length > 0) { - [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; - } - } + + // Cache the data + [self cacheBookmarkData:data forSite:site username:username host:host]; + } + + // Parse XML data + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *post in self.posts) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSString *tagString = [post objectForKey:@"tag"]; + if (tagString.length > 0) { + [tagSet + addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; } + } } - - // Create tag objects if requested - if (includeTags) { - for (NSString *tag in tagSet) { - if (tag.length > 0) { - QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [SocialSiteHelper displayNameForSite:site], tag]]; - - [tagObject setObject:tag forType:kTagType]; - [tagObject setObject:@(site) forMeta:kTagSiteField]; - [tagObject setObject:username forMeta:kTagUsernameField]; - [tagObject setObject:host forMeta:kTagHostField]; - [tagObject setObject:identifier forMeta:kTagIdentifierField]; - [tagObject setName:tag]; - [tagObject setPrimaryType:kTagType]; - [objects addObject:tagObject]; - } - } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject + makeObjectWithIdentifier: + [NSString + stringWithFormat:@"[%@ tag]:%@", + [SocialSiteHelper displayNameForSite:site], + tag]]; + + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; + [tagObject setObject:identifier forMeta:kTagIdentifierField]; + [tagObject setName:tag]; + [tagObject setPrimaryType:kTagType]; + [objects addObject:tagObject]; + } } - - return objects; + } + + return objects; } -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSData *data = [self cachedBookmarkDataForSite:site username:username host:host]; - if (!data) return @[]; - - NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; - [postParser setDelegate:self]; - self.posts = [NSMutableArray arrayWithCapacity:1]; - [postParser parse]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - - for (NSDictionary *post in self.posts) { - NSString *postTags = [post objectForKey:@"tag"]; - if ([postTags rangeOfString:tag].location != NSNotFound) { - QSObject *newObject = [self objectForPost:post]; - if (newObject) { - [objects addObject:newObject]; - } - } +- (NSArray *)fetchBookmarksForTag:(NSString *)tag + site:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForSite:site + username:username + host:host]; + if (!data) + return @[]; + + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *post in self.posts) { + NSString *postTags = [post objectForKey:@"tag"]; + if ([postTags rangeOfString:tag].location != NSNotFound) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + } } - - return objects; + } + + return objects; } - (QSObject *)objectForPost:(NSDictionary *)post { - QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; - [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; - [newObject setName:[post objectForKey:@"description"]]; - [newObject setDetails:[post objectForKey:@"extended"]]; - [newObject setPrimaryType:QSURLType]; - return newObject; + QSObject *newObject = + [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; + [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; + [newObject setName:[post objectForKey:@"description"]]; + [newObject setDetails:[post objectForKey:@"extended"]]; + [newObject setPrimaryType:QSURLType]; + return newObject; } #pragma mark - NSXMLParserDelegate -- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { - if ([elementName isEqualToString:@"post"] && attributeDict) { - [self.posts addObject:attributeDict]; - } +- (void)parser:(NSXMLParser *)parser + didStartElement:(NSString *)elementName + namespaceURI:(NSString *)namespaceURI + qualifiedName:(NSString *)qName + attributes:(NSDictionary *)attributeDict { + if ([elementName isEqualToString:@"post"] && attributeDict) { + [self.posts addObject:attributeDict]; + } } -- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - // Implementation if needed +- (void)parser:(NSXMLParser *)parser + didEndElement:(NSString *)elementName + namespaceURI:(NSString *)namespaceURI + qualifiedName:(NSString *)qName { + // Implementation if needed } @end diff --git a/Providers/QSLinkdingProvider.h b/Providers/QSLinkdingProvider.h index 28807e8..0537a0c 100644 --- a/Providers/QSLinkdingProvider.h +++ b/Providers/QSLinkdingProvider.h @@ -5,12 +5,15 @@ // Provider for Linkding API (JSON-based with API key) // -#import #import "QSBookmarkProvider.h" +#import @interface QSLinkdingProvider : NSObject -- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username; -- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username; +- (NSData *)cachedBookmarkDataForHost:(NSString *)host + username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data + forHost:(NSString *)host + username:(NSString *)username; @end \ No newline at end of file diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m index c7bf33f..378ae6f 100644 --- a/Providers/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -4,178 +4,230 @@ // #import "QSLinkdingProvider.h" -#import "SocialSite.h" #import "Constants.h" +#import "SocialSite.h" #import @implementation QSLinkdingProvider -- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - return (site == SocialSiteLinkding) && - username.length > 0 && - password.length > 0 && // password is API token for Linkding - host.length > 0; +- (BOOL)canHandleSite:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + return (site == SocialSiteLinkding) && username.length > 0 && + password.length > 0 && // password is API token for Linkding + host.length > 0; } -- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { - // Create a safe filename from host - NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; - return [NSData dataWithContentsOfFile:cachePath]; +- (NSData *)cachedBookmarkDataForHost:(NSString *)host + username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = + [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet + alphanumericCharacterSet] + invertedSet]] + componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath( + [NSString stringWithFormat:@"Caches/linkding/"], NO) + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", + safeHost, + username]]; + return [NSData dataWithContentsOfFile:cachePath]; } -- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username { - // Create a safe filename from host - NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; - NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; - [data writeToFile:cachePath atomically:NO]; +- (void)cacheBookmarkData:(NSData *)data + forHost:(NSString *)host + username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = + [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet + alphanumericCharacterSet] + invertedSet]] + componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath( + [NSString stringWithFormat:@"Caches/linkding/"], YES) + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", + safeHost, + username]]; + [data writeToFile:cachePath atomically:NO]; } -- (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 @[]; +- (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 @[]; + } + + // Try cached data first + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + + // If no cached data, fetch from API + if (![data length]) { + // Construct Linkding API URL + NSString *baseURL = host; + if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { + baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; } - - // Try cached data first - NSData *data = [self cachedBookmarkDataForHost:host username:username]; - - // If no cached data, fetch from API - if (![data length]) { - // Construct Linkding API URL - NSString *baseURL = host; - if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { - baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; - } - if ([baseURL hasSuffix:@"/"]) { - baseURL = [baseURL substringToIndex:[baseURL length] - 1]; - } - - NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; - NSURL *requestURL = [NSURL URLWithString:urlString]; - - if (!requestURL) { - NSLog(@"Invalid Linkding host URL: %@", host); - return @[]; - } - - NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:60.0]; - [theRequest setHTTPMethod:@"GET"]; - [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"]; - [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; - - NSError *error = nil; - data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; - - if (error) { - NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription); - return @[]; - } - - // Cache the data - [self cacheBookmarkData:data forHost:host username:username]; + if ([baseURL hasSuffix:@"/"]) { + baseURL = [baseURL substringToIndex:[baseURL length] - 1]; } - - // Parse JSON data - NSError *jsonError = nil; - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - - if (jsonError) { - NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); - return @[]; + + NSString *urlString = + [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; + NSURL *requestURL = [NSURL URLWithString:urlString]; + + if (!requestURL) { + NSLog(@"Invalid Linkding host URL: %@", host); + return @[]; } - - NSArray *results = [jsonResponse objectForKey:@"results"]; - if (!results || ![results isKindOfClass:[NSArray class]]) { - NSLog(@"Invalid Linkding response format"); - return @[]; + + NSMutableURLRequest *theRequest = + [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"GET"]; + [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] + forHTTPHeaderField:@"Authorization"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" + forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest + returningResponse:nil + error:&error]; + + if (error) { + NSLog(@"Error fetching Linkding bookmarks: %@", + error.localizedDescription); + return @[]; } - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - NSMutableSet *tagSet = [NSMutableSet set]; - - // Create bookmark objects - for (NSDictionary *bookmark in results) { - QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; - if (newObject) { - [objects addObject:newObject]; - - // Collect tags if requested - if (includeTags) { - NSArray *tags = [bookmark objectForKey:@"tag_names"]; - if (tags && [tags isKindOfClass:[NSArray class]]) { - [tagSet addObjectsFromArray:tags]; - } - } + + // Cache the data + [self cacheBookmarkData:data forHost:host username:username]; + } + + // Parse JSON data + NSError *jsonError = nil; + NSDictionary *jsonResponse = + [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) { + NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); + return @[]; + } + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) { + NSLog(@"Invalid Linkding response format"); + return @[]; + } + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *bookmark in results) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]]) { + [tagSet addObjectsFromArray:tags]; } + } } - - // Create tag objects if requested - if (includeTags) { - for (NSString *tag in tagSet) { - if (tag.length > 0) { - QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; - [tagObject setObject:tag forType:kTagType]; - [tagObject setObject:@(site) forMeta:kTagSiteField]; - [tagObject setObject:username forMeta:kTagUsernameField]; - [tagObject setObject:host forMeta:kTagHostField]; - // We need the identifier to be able to fetch the keychain password - [tagObject setObject:identifier forMeta:kTagIdentifierField]; - [tagObject setName:tag]; - [tagObject setPrimaryType:kTagType]; - [objects addObject:tagObject]; - } - } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject + makeObjectWithIdentifier:[NSString + stringWithFormat:@"[Linkding tag]:%@", + tag]]; + [tagObject setObject:tag forType:kTagType]; + [tagObject setObject:@(site) forMeta:kTagSiteField]; + [tagObject setObject:username forMeta:kTagUsernameField]; + [tagObject setObject:host forMeta:kTagHostField]; + // We need the identifier to be able to fetch the keychain password + [tagObject setObject:identifier forMeta:kTagIdentifierField]; + [tagObject setName:tag]; + [tagObject setPrimaryType:kTagType]; + [objects addObject:tagObject]; + } } - - return objects; + } + + return objects; } -- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { - NSData *data = [self cachedBookmarkDataForHost:host username:username]; - if (!data) return @[]; - - NSError *jsonError; - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - - if (jsonError) return @[]; - - NSArray *results = [jsonResponse objectForKey:@"results"]; - if (!results || ![results isKindOfClass:[NSArray class]]) return @[]; - - NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; - - for (NSDictionary *bookmark in results) { - NSArray *tags = [bookmark objectForKey:@"tag_names"]; - if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) { - QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; - if (newObject) { - [objects addObject:newObject]; - } - } +- (NSArray *)fetchBookmarksForTag:(NSString *)tag + site:(SocialSite)site + username:(NSString *)username + password:(NSString *)password + host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + if (!data) + return @[]; + + NSError *jsonError; + NSDictionary *jsonResponse = + [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) + return @[]; + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) + return @[]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *bookmark in results) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]] && + [tags containsObject:tag]) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + } } - - return objects; + } + + return objects; } - (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark { - NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; - NSString *url = [bookmark objectForKey:@"url"]; - NSString *title = [bookmark objectForKey:@"title"]; - NSString *description = [bookmark objectForKey:@"description"]; - - if (!bookmarkId || !url) return nil; - - QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]]; - [newObject setObject:url forType:QSURLType]; - [newObject setName:title.length > 0 ? title : url]; - [newObject setDetails:description.length > 0 ? description : @""]; - [newObject setPrimaryType:QSURLType]; - - return newObject; + NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; + NSString *url = [bookmark objectForKey:@"url"]; + NSString *title = [bookmark objectForKey:@"title"]; + NSString *description = [bookmark objectForKey:@"description"]; + + if (!bookmarkId || !url) + return nil; + + QSObject *newObject = [QSObject + makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", + bookmarkId]]; + [newObject setObject:url forType:QSURLType]; + [newObject setName:title.length > 0 ? title : url]; + [newObject setDetails:description.length > 0 ? description : @""]; + [newObject setPrimaryType:QSURLType]; + + return newObject; } @end diff --git a/QSDeliciousPlugIn_Source.h b/QSDeliciousPlugIn_Source.h index 56456c5..26d613c 100644 --- a/QSDeliciousPlugIn_Source.h +++ b/QSDeliciousPlugIn_Source.h @@ -6,18 +6,18 @@ // Copyright __MyCompanyName__ 2004. All rights reserved. // -#import -#import -#import "SocialSite.h" #import "QSBookmarkProvider.h" #import "QSBookmarkProviderFactory.h" +#import "SocialSite.h" +#import +#import @interface QSDeliciousPlugIn_Source : QSObjectSource { IBOutlet NSTextField *userField; IBOutlet NSTextField *passField; IBOutlet NSTextField *hostField; } -@property (nonatomic, strong) NSString *internalPassword; +@property(nonatomic, strong) NSString *internalPassword; - (IBAction)settingsChanged:(id)sender; @end diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 4d841a1..49526e1 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -18,28 +18,31 @@ @implementation QSDeliciousPlugIn_Source // This method will get called whenever we change which // active entry is selected. - (void)setSelectedEntry:(id)selectedEntry { - [super setSelectedEntry:selectedEntry]; - [self loadPasswordFromKeychain]; + [super setSelectedEntry:selectedEntry]; + [self loadPasswordFromKeychain]; } #pragma mark - Quicksilver Source Methods -- (BOOL)indexIsValidFromDate:(NSDate *)indexDate forEntry:(NSDictionary *)theEntry { +- (BOOL)indexIsValidFromDate:(NSDate *)indexDate + forEntry:(NSDictionary *)theEntry { return -[indexDate timeIntervalSinceNow] < 24 * 60 * 60; } -- (BOOL)isVisibleSource{ - return YES; +- (BOOL)isVisibleSource { + return YES; } -- (NSImage *) iconForEntry:(NSDictionary *)dict { - return [[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]; +- (NSImage *)iconForEntry:(NSDictionary *)dict { + return [[NSBundle bundleForClass:[self class]] imageNamed:@"bookmark_icon"]; } -- (NSView *)settingsView -{ +- (NSView *)settingsView { if (![super settingsView]) { - [[NSBundle bundleForClass:[self class]] loadNibNamed:NSStringFromClass([self class]) owner:self topLevelObjects:NULL]; + [[NSBundle bundleForClass:[self class]] + loadNibNamed:NSStringFromClass([self class]) + owner:self + topLevelObjects:NULL]; } return [super settingsView]; } @@ -48,7 +51,9 @@ - (NSView *)settingsView - (SocialSite)siteIndex { NSDictionary *settings = self.selectedEntry.sourceSettings; - return [settings objectForKey:@"site"] != nil ? [[settings objectForKey:@"site"] integerValue] : SocialSiteDelicious; + return [settings objectForKey:@"site"] != nil + ? [[settings objectForKey:@"site"] integerValue] + : SocialSiteDelicious; } - (NSString *)currentUsername { @@ -64,221 +69,251 @@ - (NSString *)currentPassword { } - (BOOL)includeTags { - return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] boolValue]; + return [[self.selectedEntry.sourceSettings objectForKey:@"includeTags"] + boolValue]; } - // 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]; + [[NSNotificationCenter defaultCenter] + postNotificationName:QSCatalogEntryChangedNotification + object:self.selectedEntry]; [self willChangeValueForKey:@"isHostVisible"]; [self didChangeValueForKey:@"isHostVisible"]; } - #pragma mark - Keychain Helper Methods - (NSString *)keychainKeyForIdentifier:(NSString *)identifier { - return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; + return [NSString stringWithFormat:@"QSSocialBookmarks-%@", identifier]; } - (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; + 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; } -- (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; +- (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; } - (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; + 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; } #pragma mark - Password Keychain Methods - (void)loadPasswordFromKeychain { - if (!self.selectedEntry || !self.selectedEntry.identifier) { - self.internalPassword = nil; - return; - } - - NSString *keychainKey = [self keychainKeyForIdentifier:self.selectedEntry.identifier]; - [self setPassword: [self passwordFromKeychainForKey:keychainKey]]; + if (!self.selectedEntry || !self.selectedEntry.identifier) { + self.internalPassword = nil; + return; + } + + NSString *keychainKey = + [self keychainKeyForIdentifier:self.selectedEntry.identifier]; + [self setPassword:[self passwordFromKeychainForKey:keychainKey]]; } - (void)savePasswordToKeychain { - if (!self.selectedEntry || !self.selectedEntry.identifier || !self.internalPassword) { - return; - } - - 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); - } + if (!self.selectedEntry || !self.selectedEntry.identifier || + !self.internalPassword) { + return; + } + + 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); + } } #pragma mark - Password Property Accessors - (NSString *)password { - return self.internalPassword; + return self.internalPassword; } - (void)setPassword:(NSString *)password { - self.internalPassword = password; - - // Save to keychain asynchronously - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self savePasswordToKeychain]; - }); + self.internalPassword = password; + + // Save to keychain asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + [self savePasswordToKeychain]; + }); } #pragma mark - Host Visibility Control -- (BOOL) isHostVisible { - return [SocialSiteHelper hasVariableHost:[self siteIndex]]; +- (BOOL)isHostVisible { + return [SocialSiteHelper hasVariableHost:[self siteIndex]]; +} +- (void)setIsHostVisible:(BOOL)isVisible { } -- (void)setIsHostVisible:(BOOL)isVisible { } #pragma mark - Objects For Entry - (NSArray *)objectsForEntry:(QSCatalogEntry *)theEntry { - - 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"]; - - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - id 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 identifier:identifier host:host includeTags:includeTags]; -} -- (NSArray *)objectsForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username identifier:(NSString *)identifier host:(NSString *)host { - + 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"]; + + QSBookmarkProviderFactory *factory = + [QSBookmarkProviderFactory sharedFactory]; + id provider = [factory providerForSite:site + username:username + password:password + host:host]; - QSBookmarkProviderFactory *factory = [QSBookmarkProviderFactory sharedFactory]; - id provider = [factory providerForSite:site username:username password:password host:host]; - if (!provider) { - NSLog(@"No provider available for site %ld with username %@", (long)site, username); + NSLog(@"No provider available for site %ld with username %@", (long)site, + username); return @[]; } - - // Check if provider supports tag-based fetching - if ([provider respondsToSelector:@selector(fetchBookmarksForTag:site:username:password:host:)]) { - return [provider fetchBookmarksForTag:tag site:site username:username password:password host:host]; - } - + + return [provider fetchBookmarksForSite:site + username:username + password:password + identifier:identifier + host:host + includeTags:includeTags]; +} + +- (NSArray *)objectsForTag:(NSString *)tag + site:(SocialSite)site + username:(NSString *)username + identifier:(NSString *)identifier + host:(NSString *)host { + + NSString *keychainKey = [self keychainKeyForIdentifier:identifier]; + NSString *password = [self passwordFromKeychainForKey:keychainKey]; + + QSBookmarkProviderFactory *factory = + [QSBookmarkProviderFactory sharedFactory]; + id 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 @[]; + } + + // Check if provider supports tag-based fetching + if ([provider respondsToSelector:@selector + (fetchBookmarksForTag:site:username:password:host:)]) { + return [provider fetchBookmarksForTag:tag + site:site + username:username + password:password + host:host]; + } + + return @[]; } #pragma mark - Object Handler Methods - (void)setQuickIconForObject:(QSObject *)object { if (@available(macOS 11.0, *)) { - NSImage *image = [NSImage imageWithSystemSymbolName:@"tag" accessibilityDescription:@"Bookmark"]; + NSImage *image = [NSImage imageWithSystemSymbolName:@"tag" + accessibilityDescription:@"Bookmark"]; [object setIcon:image]; } else { - [object setIcon:[[NSBundle bundleForClass:[self class]]imageNamed:@"bookmark_icon"]]; + [object setIcon:[[NSBundle bundleForClass:[self class]] + imageNamed:@"bookmark_icon"]]; } } -// All our objects will have children. URLs will have tags, and tags will have URLs. -- (BOOL)objectHasChildren:(QSObject *) object { return YES; } +// All our objects will have children. URLs will have tags, and tags will have +// URLs. +- (BOOL)objectHasChildren:(QSObject *)object { + return YES; +} // 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 { - + 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."); @@ -302,9 +337,13 @@ - (BOOL)loadChildrenForObject:(QSObject *)object { return NO; } - NSArray *children = [self objectsForTag:tag site:site username:username identifier:identifier host:host]; - [object setChildren:children]; - return YES; + NSArray *children = [self objectsForTag:tag + site:site + username:username + identifier:identifier + host:host]; + [object setChildren:children]; + return YES; } @end diff --git a/Types/SocialSite.h b/Types/SocialSite.h index 43155c7..2510594 100644 --- a/Types/SocialSite.h +++ b/Types/SocialSite.h @@ -8,11 +8,11 @@ #import typedef NS_ENUM(NSInteger, SocialSite) { - SocialSiteDelicious = 0, - SocialSiteMagnolia = 1, - SocialSitePinboard = 2, - SocialSiteLinkding = 3, - SocialSiteSelfHostedDeliciousCompatible = 4 + SocialSiteDelicious = 0, + SocialSiteMagnolia = 1, + SocialSitePinboard = 2, + SocialSiteLinkding = 3, + SocialSiteSelfHostedDeliciousCompatible = 4 }; @interface SocialSiteHelper : NSObject diff --git a/Types/SocialSite.m b/Types/SocialSite.m index 369f86d..258386d 100644 --- a/Types/SocialSite.m +++ b/Types/SocialSite.m @@ -8,48 +8,48 @@ @implementation SocialSiteHelper + (NSString *)displayNameForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"del.icio.us"; - case SocialSiteMagnolia: - return @"ma.gnolia.com"; - case SocialSitePinboard: - return @"Pinboard"; - case SocialSiteLinkding: - return @"Linkding"; - case SocialSiteSelfHostedDeliciousCompatible: - return @"Self-Hosted (Delicious Compatible)"; - default: - return @"Unknown"; - } + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"Pinboard"; + case SocialSiteLinkding: + return @"Linkding"; + case SocialSiteSelfHostedDeliciousCompatible: + return @"Self-Hosted (Delicious Compatible)"; + default: + return @"Unknown"; + } } // This is used for caching key + (NSString *)cacheKeyForSite:(SocialSite)site { - switch (site) { - case SocialSiteDelicious: - return @"del.icio.us"; - case SocialSiteMagnolia: - return @"ma.gnolia.com"; - case SocialSitePinboard: - return @"pinboard.in"; - case SocialSiteSelfHostedDeliciousCompatible: - return @"self-hosted-delicious"; - case SocialSiteLinkding: - return @"linkding"; - default: - return nil; - } + switch (site) { + case SocialSiteDelicious: + return @"del.icio.us"; + case SocialSiteMagnolia: + return @"ma.gnolia.com"; + case SocialSitePinboard: + return @"pinboard.in"; + case SocialSiteSelfHostedDeliciousCompatible: + return @"self-hosted-delicious"; + case SocialSiteLinkding: + return @"linkding"; + default: + return nil; + } } + (BOOL)hasVariableHost:(SocialSite)site { - switch (site) { - case SocialSiteSelfHostedDeliciousCompatible: - case SocialSiteLinkding: - return YES; - default: - return NO; - } + switch (site) { + case SocialSiteSelfHostedDeliciousCompatible: + case SocialSiteLinkding: + return YES; + default: + return NO; + } } @end From 2d7a7f25cbd4b2ecdd326b8254eb94566d91e198 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 22:09:42 +0200 Subject: [PATCH 08/11] Keep the original images --- Providers/QSLinkdingProvider.m | 5 ++++- QSDeliciousPlugIn_Source.m | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m index 378ae6f..33e82bc 100644 --- a/Providers/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -80,8 +80,11 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site baseURL = [baseURL substringToIndex:[baseURL length] - 1]; } + // I'm being lazy with the limit for now. This should instead loop while + // there is a next, but then the caching will also need to be changed. If + // you have a particularly large linkding library, I apologize. NSString *urlString = - [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; + [NSString stringWithFormat:@"%@/api/bookmarks/?limit=10000", baseURL]; NSURL *requestURL = [NSURL URLWithString:urlString]; if (!requestURL) { diff --git a/QSDeliciousPlugIn_Source.m b/QSDeliciousPlugIn_Source.m index 49526e1..e864514 100644 --- a/QSDeliciousPlugIn_Source.m +++ b/QSDeliciousPlugIn_Source.m @@ -285,14 +285,8 @@ - (NSArray *)objectsForTag:(NSString *)tag #pragma mark - Object Handler Methods - (void)setQuickIconForObject:(QSObject *)object { - if (@available(macOS 11.0, *)) { - NSImage *image = [NSImage imageWithSystemSymbolName:@"tag" - accessibilityDescription:@"Bookmark"]; - [object setIcon:image]; - } else { - [object setIcon:[[NSBundle bundleForClass:[self class]] - imageNamed:@"bookmark_icon"]]; - } + [object setIcon:[[NSBundle bundleForClass:[self class]] + imageNamed:@"bookmark_icon"]]; } // All our objects will have children. URLs will have tags, and tags will have From f7ad895456293dd09c4c582022abcc5f0d2cb2f0 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 22:26:32 +0200 Subject: [PATCH 09/11] Fix typo --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 5a08e8e..edfd9d2 100644 --- a/README.markdown +++ b/README.markdown @@ -66,6 +66,6 @@ Supported Services This plugin supports Delicious compatible services (eg. Linkhut), and Linkding. It's important to note that delicious compatible systems aren't all compatible in the same way, so you might need to make some adjustments: -- For every self-hsoted service, make sure to include the protocol (eg. https://). +- For every self-hosted service, make sure to include the protocol (eg. https://). - For delicious compatible items, include everything before `/v1`. So if it's `https://api.ln.ht/v1` you just include `https://api.ln.ht`. But if it's `https://myservice.com/api/v1` then you should include `https://myservice.com/api` with no trailing slash. - For delicious compatible items, check what they expect for the `auth_token` field and use that as your password. For example, in pinboard it's `username:token`, while in linkhut it's just `token`. From 3d9b0bd09378adc950ef7de8064a4ef852ba8ecd Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 22:28:20 +0200 Subject: [PATCH 10/11] Remove some comments --- Providers/QSDeliciousAPIProvider.h | 1 - Providers/QSDeliciousAPIProvider.m | 7 ------- Providers/QSLinkdingProvider.m | 8 -------- 3 files changed, 16 deletions(-) diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h index 310b00b..1628c38 100644 --- a/Providers/QSDeliciousAPIProvider.h +++ b/Providers/QSDeliciousAPIProvider.h @@ -14,7 +14,6 @@ @property(nonatomic, strong) NSMutableArray *posts; -// Subclasses can override these methods - (NSString *)apiURLForSite:(SocialSite)site andHost:(NSString *)host; - (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m index 03991c1..a697618 100644 --- a/Providers/QSDeliciousAPIProvider.m +++ b/Providers/QSDeliciousAPIProvider.m @@ -114,12 +114,10 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site host:(NSString *)host includeTags:(BOOL)includeTags { - // Try cached data first NSData *data = [self cachedBookmarkDataForSite:site username:username host:host]; - // If no cached data, fetch from API if (![data length]) { NSURL *requestURL = [self requestURLForSite:site username:username @@ -147,11 +145,9 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site return @[]; } - // Cache the data [self cacheBookmarkData:data forSite:site username:username host:host]; } - // Parse XML data NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; [postParser setDelegate:self]; @@ -161,13 +157,11 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; NSMutableSet *tagSet = [NSMutableSet set]; - // Create bookmark objects for (NSDictionary *post in self.posts) { QSObject *newObject = [self objectForPost:post]; if (newObject) { [objects addObject:newObject]; - // Collect tags if requested if (includeTags) { NSString *tagString = [post objectForKey:@"tag"]; if (tagString.length > 0) { @@ -178,7 +172,6 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site } } - // Create tag objects if requested if (includeTags) { for (NSString *tag in tagSet) { if (tag.length > 0) { diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m index 33e82bc..32b4c16 100644 --- a/Providers/QSLinkdingProvider.m +++ b/Providers/QSLinkdingProvider.m @@ -66,12 +66,9 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site return @[]; } - // Try cached data first NSData *data = [self cachedBookmarkDataForHost:host username:username]; - // If no cached data, fetch from API if (![data length]) { - // Construct Linkding API URL NSString *baseURL = host; if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; @@ -114,11 +111,9 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site return @[]; } - // Cache the data [self cacheBookmarkData:data forHost:host username:username]; } - // Parse JSON data NSError *jsonError = nil; NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; @@ -137,13 +132,11 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; NSMutableSet *tagSet = [NSMutableSet set]; - // Create bookmark objects for (NSDictionary *bookmark in results) { QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; if (newObject) { [objects addObject:newObject]; - // Collect tags if requested if (includeTags) { NSArray *tags = [bookmark objectForKey:@"tag_names"]; if (tags && [tags isKindOfClass:[NSArray class]]) { @@ -153,7 +146,6 @@ - (NSArray *)fetchBookmarksForSite:(SocialSite)site } } - // Create tag objects if requested if (includeTags) { for (NSString *tag in tagSet) { if (tag.length > 0) { From ae7217421af002b166252d9b93b29d4095f3d912 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 8 Sep 2025 22:31:20 +0200 Subject: [PATCH 11/11] Bump version --- Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Info.plist b/Info.plist index 5101e87..e84ffd7 100644 --- a/Info.plist +++ b/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2.1.0 + 3.0.0 CFBundleVersion - 131 + 150 NSPrincipalClass QSDeliciousPlugIn QSPlugIn