diff --git a/ownCloudSDK.xcodeproj/project.pbxproj b/ownCloudSDK.xcodeproj/project.pbxproj index 5067d0e3..0b5134f8 100644 --- a/ownCloudSDK.xcodeproj/project.pbxproj +++ b/ownCloudSDK.xcodeproj/project.pbxproj @@ -434,6 +434,11 @@ DC6D38802C4E4ED300169BF5 /* OCWaitConditionAvailableOffline.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6D387E2C4E4ED300169BF5 /* OCWaitConditionAvailableOffline.m */; }; DC6D51D924A8BC4D006B75E6 /* OCNetworkMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6D51D724A8BC4C006B75E6 /* OCNetworkMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC6D51DA24A8BC4D006B75E6 /* OCNetworkMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6D51D824A8BC4C006B75E6 /* OCNetworkMonitor.m */; }; + DC6D937D2CD5762B00537645 /* OCConnection+Search.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6D937B2CD5762B00537645 /* OCConnection+Search.m */; }; + DC6D93822CD6BEC900537645 /* OCCore+Search.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6D93802CD6BEC900537645 /* OCCore+Search.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC6D93832CD6BEC900537645 /* OCCore+Search.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6D93812CD6BEC900537645 /* OCCore+Search.m */; }; + DC6D93862CD6BF3200537645 /* OCSearchResult.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6D93842CD6BF3200537645 /* OCSearchResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC6D93872CD6BF3200537645 /* OCSearchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6D93852CD6BF3200537645 /* OCSearchResult.m */; }; DC6DB88221C26F4D00189B21 /* XCTestCase+Tagging.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6DB88121C26F4D00189B21 /* XCTestCase+Tagging.m */; }; DC6DEEAD24C5978E00E3772E /* OCHTTPPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6DEEAB24C5978E00E3772E /* OCHTTPPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC6DEEAE24C5978E00E3772E /* OCHTTPPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6DEEAC24C5978E00E3772E /* OCHTTPPolicy.m */; }; @@ -879,6 +884,8 @@ DCF00BF527E28A77001F2AFC /* OCDataSourceSubscription+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF00BF327E28A77001F2AFC /* OCDataSourceSubscription+Internal.h */; }; DCF00BF627E28A77001F2AFC /* OCDataSourceSubscription+Internal.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF00BF427E28A77001F2AFC /* OCDataSourceSubscription+Internal.m */; }; DCF00C1927E698A4001F2AFC /* DataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF00C1827E698A4001F2AFC /* DataSourceTests.m */; }; + DCF06B662CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF06B642CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCF06B672CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF06B652CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.m */; }; DCF072E42798630900E0B01D /* OCResourceTextPlaceholder.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF072E22798630900E0B01D /* OCResourceTextPlaceholder.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCF072E52798630900E0B01D /* OCResourceTextPlaceholder.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF072E32798630900E0B01D /* OCResourceTextPlaceholder.m */; }; DCF072E82798652500E0B01D /* OCResourceSourceAvatarPlaceholders.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF072E62798652500E0B01D /* OCResourceSourceAvatarPlaceholders.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1456,6 +1463,11 @@ DC6D387E2C4E4ED300169BF5 /* OCWaitConditionAvailableOffline.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCWaitConditionAvailableOffline.m; sourceTree = ""; }; DC6D51D724A8BC4C006B75E6 /* OCNetworkMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCNetworkMonitor.h; sourceTree = ""; }; DC6D51D824A8BC4C006B75E6 /* OCNetworkMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCNetworkMonitor.m; sourceTree = ""; }; + DC6D937B2CD5762B00537645 /* OCConnection+Search.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCConnection+Search.m"; sourceTree = ""; }; + DC6D93802CD6BEC900537645 /* OCCore+Search.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCCore+Search.h"; sourceTree = ""; }; + DC6D93812CD6BEC900537645 /* OCCore+Search.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCCore+Search.m"; sourceTree = ""; }; + DC6D93842CD6BF3200537645 /* OCSearchResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSearchResult.h; sourceTree = ""; }; + DC6D93852CD6BF3200537645 /* OCSearchResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSearchResult.m; sourceTree = ""; }; DC6DB88021C26F4D00189B21 /* XCTestCase+Tagging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Tagging.h"; sourceTree = ""; }; DC6DB88121C26F4D00189B21 /* XCTestCase+Tagging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Tagging.m"; sourceTree = ""; }; DC6DEEAB24C5978E00E3772E /* OCHTTPPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCHTTPPolicy.h; sourceTree = ""; }; @@ -1919,6 +1931,8 @@ DCF00BF327E28A77001F2AFC /* OCDataSourceSubscription+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCDataSourceSubscription+Internal.h"; sourceTree = ""; }; DCF00BF427E28A77001F2AFC /* OCDataSourceSubscription+Internal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCDataSourceSubscription+Internal.m"; sourceTree = ""; }; DCF00C1827E698A4001F2AFC /* DataSourceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DataSourceTests.m; sourceTree = ""; }; + DCF06B642CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCQueryCondition+KQLBuilder.h"; sourceTree = ""; }; + DCF06B652CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCQueryCondition+KQLBuilder.m"; sourceTree = ""; }; DCF072E22798630900E0B01D /* OCResourceTextPlaceholder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCResourceTextPlaceholder.h; sourceTree = ""; }; DCF072E32798630900E0B01D /* OCResourceTextPlaceholder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCResourceTextPlaceholder.m; sourceTree = ""; }; DCF072E62798652500E0B01D /* OCResourceSourceAvatarPlaceholders.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCResourceSourceAvatarPlaceholders.h; sourceTree = ""; }; @@ -2589,6 +2603,8 @@ DC359690223FA7CC00C4D6E6 /* OCQueryCondition.h */, DC35969522403E5B00C4D6E6 /* OCQueryCondition+SQLBuilder.m */, DC35969422403E5B00C4D6E6 /* OCQueryCondition+SQLBuilder.h */, + DCF06B652CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.m */, + DCF06B642CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.h */, DC3596992240EC0A00C4D6E6 /* OCQueryCondition+Item.m */, DC3596982240EC0A00C4D6E6 /* OCQueryCondition+Item.h */, ); @@ -3031,6 +3047,25 @@ path = Update; sourceTree = ""; }; + DC6D937E2CD5879A00537645 /* Search */ = { + isa = PBXGroup; + children = ( + DC6D937B2CD5762B00537645 /* OCConnection+Search.m */, + ); + path = Search; + sourceTree = ""; + }; + DC6D937F2CD6BE9C00537645 /* Search */ = { + isa = PBXGroup; + children = ( + DC6D93812CD6BEC900537645 /* OCCore+Search.m */, + DC6D93802CD6BEC900537645 /* OCCore+Search.h */, + DC6D93852CD6BF3200537645 /* OCSearchResult.m */, + DC6D93842CD6BF3200537645 /* OCSearchResult.h */, + ); + path = Search; + sourceTree = ""; + }; DC6DEEAA24C5977500E3772E /* Policy */ = { isa = PBXGroup; children = ( @@ -3323,6 +3358,7 @@ DC19BFE221CB9FAC007C20D1 /* FileProvider */, DC2FED5D228D5577004FDEC6 /* Favorites */, DC19BFE121CB9F4F007C20D1 /* Sync */, + DC6D937F2CD6BE9C00537645 /* Search */, DC2F6377223A61A60063C2DA /* Core Query */, ); path = Core; @@ -3402,6 +3438,7 @@ DCCE49362684BBF5005961D8 /* DAVResponse */, DCD63279223BB1930090169E /* Capabilities */, DC85570A204FEA5E00189B9A /* Categories */, + DC6D937E2CD5879A00537645 /* Search */, DCDB760F2739D26100EE7A06 /* ServerLocator */, ); path = Connection; @@ -4480,6 +4517,7 @@ DC4B1171220830F20062BCDD /* OCHTTPPipelineBackend.h in Headers */, DC19BFD221CA6C15007C20D1 /* OCSyncIssueChoice.h in Headers */, DCDB761E2739D4A300EE7A06 /* OCServerLocatorWebFinger.h in Headers */, + DC6D93822CD6BEC900537645 /* OCCore+Search.h in Headers */, DCEE0B6F25E697AF006534B5 /* OCCoreManager+ItemResolution.h in Headers */, DCC8FA21202B218100EB6701 /* OCAppIdentity.h in Headers */, DCC3701324D4D134008B0DEB /* OCScanJobActivity.h in Headers */, @@ -4600,6 +4638,7 @@ DC41C79025EA5F7A0074F23B /* OCResourceSource.h in Headers */, DCF575DF27956D84003BEBBA /* OCViewProviderContext.h in Headers */, DC701477220AE696009D4FD9 /* OCHTTPPipelineTask.h in Headers */, + DC6D93862CD6BF3200537645 /* OCSearchResult.h in Headers */, DC47E4DD27A5820D0020E8EF /* GAODataError.h in Headers */, DC622C4E29019515001D73A0 /* OCLocale+SystemLanguage.h in Headers */, DC47E4D727A5820D0020E8EF /* GAQuota.h in Headers */, @@ -4781,6 +4820,7 @@ DCAEB06921FA617D0067E147 /* OCActivity.h in Headers */, DCFFF57E20D3A51C0096D2D3 /* OCSyncContext.h in Headers */, DC0283632090A3E8005B6334 /* OCItemThumbnail.h in Headers */, + DCF06B662CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.h in Headers */, DCADC04D2072D54200DB8E83 /* OCSQLiteTableSchema.h in Headers */, DC73F3BF254BFE9900CE5FA9 /* NSArray+ObjCRuntime.h in Headers */, DCC4F3FF27D75BF700ABF4C9 /* OCDataConverterPipeline.h in Headers */, @@ -5246,6 +5286,7 @@ DCDD9B15222986D50052A001 /* OCShare+OCXMLObjectCreation.m in Sources */, DC54396520D50B8A002BF291 /* OCCore+CommandDelete.m in Sources */, DCF163F7274BA6C300E0182A /* OCSQLiteCollationLocalized.m in Sources */, + DC6D93872CD6BF3200537645 /* OCSearchResult.m in Sources */, DC4B116E2208306C0062BCDD /* OCHTTPPipeline.m in Sources */, DC8B7B3A22D88FFD00E63657 /* OCItemPolicyProcessor.m in Sources */, DC8266602818972200F91F7D /* OCVaultLocation.m in Sources */, @@ -5270,6 +5311,7 @@ DC7014262209CE7A009D4FD9 /* OCHTTPPipelineManager.m in Sources */, DC6ABF6C2534633E00689C7B /* OCHostSimulatorManager.m in Sources */, DCD2D40422F059190071FB8F /* OCClassSettingsUserPreferences.m in Sources */, + DC6D937D2CD5762B00537645 /* OCConnection+Search.m in Sources */, DC5D9E6924963DED00BFFE8E /* OCMessageChoice.m in Sources */, DCC26FBA2B722C8900904000 /* OCPasswordPolicyReport.m in Sources */, DC855700204F597800189B9A /* OCXMLParserNode.m in Sources */, @@ -5369,6 +5411,7 @@ DC0BE5B228F80BBA00CE2101 /* OCShareRole.m in Sources */, DCC26FC02B7397D400904000 /* OCPasswordPolicyRule+StandardRules.m in Sources */, DCC6567520CA695600110A97 /* OCCoreManager.m in Sources */, + DCF06B672CED34AB00B95D79 /* OCQueryCondition+KQLBuilder.m in Sources */, DC2F66A12603FCF6001BFDB6 /* OCCancelAction.m in Sources */, DC6DEEBA24C5C82400E3772E /* OCHTTPPolicyBookmark.m in Sources */, DCEE0B7025E697AF006534B5 /* OCCoreManager+ItemResolution.m in Sources */, @@ -5415,6 +5458,7 @@ DC85571D2050196000189B9A /* OCItem+OCXMLObjectCreation.m in Sources */, DC7E0A702036F28B006111FA /* OCKeychain.m in Sources */, DC826666281AC59D00F91F7D /* OCVFSCore.m in Sources */, + DC6D93832CD6BEC900537645 /* OCCore+Search.m in Sources */, DC49B55428339BE200DAF13B /* NSArray+OCMapping.m in Sources */, DC4AFAB1206A8C1D00189B9A /* OCSQLiteStatement.m in Sources */, DCEEB2F6204802CF00189B9A /* OCIssue.m in Sources */, diff --git a/ownCloudSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ownCloudSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata index a1c8c07e..919434a6 100644 --- a/ownCloudSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ownCloudSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ownCloudSDK/Connection/Capabilities/OCCapabilities.h b/ownCloudSDK/Connection/Capabilities/OCCapabilities.h index 67a92e0f..49b78083 100644 --- a/ownCloudSDK/Connection/Capabilities/OCCapabilities.h +++ b/ownCloudSDK/Connection/Capabilities/OCCapabilities.h @@ -137,6 +137,11 @@ typedef NSNumber* OCCapabilityBool; @property(readonly,nonatomic) BOOL federatedSharingSupported; +#pragma mark - Search +@property(readonly,nonatomic) BOOL serverSideSearchSupported; //!< Indicates if ocis-style KQL-based server-side search is available +@property(readonly,nullable,nonatomic) NSArray *enabledServerSideSearchProperties; //!< Returns a list of enabled/supported server-side search properties (f.ex. "name", "mtime", "size", "mediatype", "type", "tag", "tags", "content", "scope") +- (nullable NSArray *)supportedKeywordsForServerSideSearchProperty:(NSString *)searchPropertyName; //!< Returns the server-provided list of supported keywords for that property (f.ex. "document", "spreadsheet", … for "mediatype") + #pragma mark - Notifications @property(readonly,nullable,nonatomic) NSArray *notificationEndpoints; diff --git a/ownCloudSDK/Connection/Capabilities/OCCapabilities.m b/ownCloudSDK/Connection/Capabilities/OCCapabilities.m index 40dece2d..0409cc4c 100644 --- a/ownCloudSDK/Connection/Capabilities/OCCapabilities.m +++ b/ownCloudSDK/Connection/Capabilities/OCCapabilities.m @@ -122,6 +122,9 @@ @implementation OCCapabilities @dynamic federatedSharingIncoming; @dynamic federatedSharingOutgoing; +#pragma mark - Search +@dynamic serverSideSearchSupported; + #pragma mark - Notifications @dynamic notificationEndpoints; @@ -741,6 +744,59 @@ - (BOOL)federatedSharingSupported return (self.federatedSharingIncoming.boolValue || self.federatedSharingOutgoing.boolValue); } +#pragma mark - Search +- (BOOL)serverSideSearchSupported +{ + return (OCTypedCast(_capabilities[@"search"], NSDictionary) != nil); +} + +- (NSArray *)enabledServerSideSearchProperties +{ + NSDictionary *searchCapabilityDict = OCTypedCast(_capabilities[@"search"], NSDictionary); + NSMutableArray *enabledProperties = nil; + if (searchCapabilityDict != nil) + { + NSDictionary *propertyListDict = OCTypedCast(searchCapabilityDict[@"property"], NSDictionary); + if (propertyListDict != nil) + { + for (NSString *property in propertyListDict) + { + NSDictionary *propertyDict = OCTypedCast(propertyListDict[property], NSDictionary); + + if (propertyDict != nil) + { + if ([propertyDict[@"enabled"] isKindOfClass:NSNumber.class] && (((NSNumber *)propertyDict[@"enabled"]).boolValue)) + { + if (enabledProperties == nil) { enabledProperties = [NSMutableArray new]; } + [enabledProperties addObject:property]; + } + } + } + } + } + + return (enabledProperties); +} + +- (nullable NSArray *)supportedKeywordsForServerSideSearchProperty:(NSString *)searchPropertyName +{ + NSDictionary *searchCapabilityDict = OCTypedCast(_capabilities[@"search"], NSDictionary); + NSMutableArray *enabledProperties = nil; + if (searchCapabilityDict != nil) + { + NSDictionary *propertyListDict = OCTypedCast(searchCapabilityDict[@"property"], NSDictionary); + if (propertyListDict != nil) + { + NSDictionary *propertyDict = OCTypedCast(propertyListDict[searchPropertyName], NSDictionary); + if ((propertyDict != nil) && [propertyDict[@"keywords"] isKindOfClass:NSArray.class]) + { + return (propertyDict[@"keywords"]); + } + } + } + return (nil); +} + #pragma mark - Notifications - (NSArray *)notificationEndpoints { diff --git a/ownCloudSDK/Connection/OCConnection+Sharing.m b/ownCloudSDK/Connection/OCConnection+Sharing.m index 4d653d63..e63a77c3 100644 --- a/ownCloudSDK/Connection/OCConnection+Sharing.m +++ b/ownCloudSDK/Connection/OCConnection+Sharing.m @@ -944,7 +944,7 @@ - (nullable NSProgress *)retrievePrivateLinkForItem:(OCItem *)item completionHan NSArray *errors = nil; NSArray *items = nil; - if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) + if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path drives:nil reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) { NSURL *privateLink; @@ -1011,7 +1011,7 @@ - (nullable NSProgress *)retrievePathForPrivateLink:(NSURL *)privateLink complet NSArray *errors = nil; NSArray *items = nil; - if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) + if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path drives:nil reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) { OCLocation *location; diff --git a/ownCloudSDK/Connection/OCConnection.h b/ownCloudSDK/Connection/OCConnection.h index 4c67b6db..f51d8522 100644 --- a/ownCloudSDK/Connection/OCConnection.h +++ b/ownCloudSDK/Connection/OCConnection.h @@ -216,6 +216,8 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSProgress *)retrieveItemListAtLocation:(OCLocation *)location depth:(NSUInteger)depth options:(nullable OCConnectionOptions)options completionHandler:(void(^)(NSError * _Nullable error, NSArray * _Nullable items))completionHandler; //!< Retrieves the items at the specified path with options - (nullable NSProgress *)retrieveItemListAtLocation:(OCLocation *)location depth:(NSUInteger)depth options:(nullable OCConnectionOptions)options resultTarget:(OCEventTarget *)eventTarget; //!< Retrieves the items at the specified path, with options to schedule on the background queue and with a "not before" date. +- (NSMutableArray *)_davItemAttributes; //!< Returns a newly created array of XML nodes that are requested by a PROPFIND by default + #pragma mark - Actions - (nullable OCProgress *)createFolder:(NSString *)folderName inside:(OCItem *)parentItem options:(nullable OCConnectionOptions)options resultTarget:(OCEventTarget *)eventTarget; @@ -439,11 +441,18 @@ typedef void(^OCConnectionRecipientsRetrievalCompletionHandler)(NSError * _Nulla - (nullable NSError *)supportsServerVersion:(NSString *)serverVersion product:(NSString *)product longVersion:(NSString *)longVersion allowHiddenVersion:(BOOL)allowHiddenVersion; @end +@interface OCConnection (Search) + +- (nullable OCProgress *)searchFilesWithPattern:(NSString *)pattern limit:(nullable NSNumber *)limit options:(nullable NSDictionary *)options resultTarget:(OCEventTarget *)eventTarget; + +@end + extern OCConnectionEndpointID OCConnectionEndpointIDWellKnown; extern OCConnectionEndpointID OCConnectionEndpointIDCapabilities; extern OCConnectionEndpointID OCConnectionEndpointIDUser; extern OCConnectionEndpointID OCConnectionEndpointIDWebDAV; extern OCConnectionEndpointID OCConnectionEndpointIDWebDAVMeta; +extern OCConnectionEndpointID OCConnectionEndpointIDWebDAVSpaces; //!< Spaces DAV endpoint, used for f.ex. search (see ocis#9367) extern OCConnectionEndpointID OCConnectionEndpointIDWebDAVRoot; //!< Virtual, non-configurable endpoint, builds the root URL based on OCConnectionEndpointIDWebDAV and the username found in connection.loggedInUser extern OCConnectionEndpointID OCConnectionEndpointIDPreview; //!< Virtual, non-configurable endpoint, builds the root URL for requesting previews based on OCConnectionEndpointIDWebDAV, the username found in connection.loggedInUser and the drive ID extern OCConnectionEndpointID OCConnectionEndpointIDStatus; diff --git a/ownCloudSDK/Connection/OCConnection.m b/ownCloudSDK/Connection/OCConnection.m index b99c3f4a..fd59f2cd 100644 --- a/ownCloudSDK/Connection/OCConnection.m +++ b/ownCloudSDK/Connection/OCConnection.m @@ -123,6 +123,7 @@ + (OCClassSettingsIdentifier)classSettingsIdentifier OCConnectionEndpointIDUser : @"ocs/v2.php/cloud/user", // Requested once on login OCConnectionEndpointIDWebDAV : @"remote.php/dav/files", // Polled in intervals to detect changes to the root directory ETag OCConnectionEndpointIDWebDAVMeta : @"remote.php/dav/meta", // Metadata DAV endpoint, used for private link resolution + OCConnectionEndpointIDWebDAVSpaces : @"remote.php/dav/spaces", // Spaces DAV endpoint, used for f.ex. search (see ocis#9367) OCConnectionEndpointIDStatus : @"status.php", // Requested during login and polled in intervals during maintenance mode OCConnectionEndpointIDShares : @"ocs/v2.php/apps/files_sharing/api/v1/shares", // Polled in intervals to detect changes if OCShareQuery is used with the interval option OCConnectionEndpointIDRemoteShares : @"ocs/v2.php/apps/files_sharing/api/v1/remote_shares",// Polled in intervals to detect changes if OCShareQuery is used with the interval option @@ -265,6 +266,14 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata OCClassSettingsMetadataKeyFlags : @(OCClassSettingsFlagDenyUserPreferences) }, + OCConnectionEndpointIDWebDAVSpaces : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, + OCClassSettingsMetadataKeyDescription : @"Endpoint to as for WebDAV spaces.", + OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusAdvanced, + OCClassSettingsMetadataKeyCategory : @"Endpoints", + OCClassSettingsMetadataKeyFlags : @(OCClassSettingsFlagDenyUserPreferences) + }, + OCConnectionEndpointIDStatus : @{ OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, OCClassSettingsMetadataKeyDescription : @"Endpoint to retrieve basic status information and detect an ownCloud installation.", @@ -2028,7 +2037,7 @@ - (void)_handleRetrieveItemListAtPathResult:(OCHTTPRequest *)request error:(NSEr // OCLogDebug(@"Error: %@ - Response: %@", OCLogPrivate(error), ((request.downloadRequest && (request.downloadedFileURL != nil)) ? OCLogPrivate([NSString stringWithContentsOfURL:request.downloadedFileURL encoding:NSUTF8StringEncoding error:NULL]) : nil)); - items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path reuseUsersByID:_usersByUserID driveID:driveID withErrors:&errors]; + items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path drives:nil reuseUsersByID:_usersByUserID driveID:driveID withErrors:&errors]; if ((items.count == 0) && (errors.count > 0) && (event.error == nil)) { @@ -3289,7 +3298,7 @@ - (void)_handleFilterFilesResult:(OCHTTPRequest *)request error:(NSError *)error if (endpointURL != nil) { - if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) + if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path drives:nil reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) { event.result = items; } @@ -3371,6 +3380,7 @@ - (NSError *)sendSynchronousRequest:(OCHTTPRequest *)request OCConnectionEndpointID OCConnectionEndpointIDWebDAV = @"endpoint-webdav"; OCConnectionEndpointID OCConnectionEndpointIDWebDAVMeta = @"endpoint-webdav-meta"; OCConnectionEndpointID OCConnectionEndpointIDWebDAVRoot = @"endpoint-webdav-root"; +OCConnectionEndpointID OCConnectionEndpointIDWebDAVSpaces = @"endpoint-webdav-spaces"; OCConnectionEndpointID OCConnectionEndpointIDPreview = @"endpoint-preview"; OCConnectionEndpointID OCConnectionEndpointIDStatus = @"endpoint-status"; OCConnectionEndpointID OCConnectionEndpointIDShares = @"endpoint-shares"; diff --git a/ownCloudSDK/Connection/Search/OCConnection+Search.m b/ownCloudSDK/Connection/Search/OCConnection+Search.m new file mode 100644 index 00000000..e423117a --- /dev/null +++ b/ownCloudSDK/Connection/Search/OCConnection+Search.m @@ -0,0 +1,129 @@ +// +// OCConnection+Search.m +// ownCloudSDK +// +// Created by Felix Schwarz on 31.10.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCConnection.h" +#import "OCXMLNode.h" +#import "OCHTTPDAVRequest.h" +#import "NSError+OCError.h" +#import "OCMacros.h" + +@implementation OCConnection (Search) + +- (nullable OCProgress *)searchFilesWithPattern:(NSString *)pattern limit:(nullable NSNumber *)limit options:(nullable NSDictionary *)options resultTarget:(OCEventTarget *)eventTarget +{ + NSURL *endpointURL = [self URLForEndpoint:OCConnectionEndpointIDWebDAVSpaces options:nil]; + NSMutableArray *searchNodes = [NSMutableArray new]; + NSMutableArray *propertyNodes = [self _davItemAttributes]; + OCHTTPDAVRequest *request; + + if (endpointURL == nil) + { + // WebDAV root could not be generated (likely due to lack of username) + [eventTarget handleError:OCError(OCErrorInternal) type:OCEventTypeSearch uuid:nil sender:self]; + + return (nil); + } + + if (pattern != nil) + { + [searchNodes addObject:[OCXMLNode elementWithName:@"oc:pattern" stringValue:pattern]]; + } + + if (limit != nil) + { + [searchNodes addObject:[OCXMLNode elementWithName:@"oc:limit" stringValue:limit.stringValue]]; + } + + if (searchNodes.count == 0) + { + [eventTarget handleError:OCError(OCErrorInsufficientParameters) type:OCEventTypeSearch uuid:nil sender:self]; + return(nil); + } + + request = [OCHTTPDAVRequest reportRequestWithURL:endpointURL rootElementName:@"oc:search-files" content:[[NSArray alloc] initWithObjects: + [OCXMLNode elementWithName:@"D:prop" children:propertyNodes], + [OCXMLNode elementWithName:@"oc:search" children:searchNodes], + nil]]; + request.eventTarget = eventTarget; + request.resultHandlerAction = @selector(_handleSearchResult:error:); + request.requiredSignals = self.propFindSignals; + + // Attach to pipelines + [self attachToPipelines]; + + // Enqueue request + [self.ephermalPipeline enqueueRequest:request forPartitionID:self.partitionID]; + + return (request.progress); +} + +- (void)_handleSearchResult:(OCHTTPRequest *)request error:(NSError *)error +{ + OCEvent *event; + + if ((event = [OCEvent eventForEventTarget:request.eventTarget type:OCEventTypeSearch uuid:request.identifier attributes:nil]) != nil) + { + if (error != nil) + { + event.error = error; + } + else if (request.error != nil) + { + event.error = request.error; + } + else + { + if (request.httpResponse.status.isSuccess) + { + NSArray *items = nil; + NSArray *errors = nil; + NSURL *endpointURL = [self URLForEndpoint:OCConnectionEndpointIDWebDAVSpaces options:nil]; + + if (endpointURL != nil) + { + if ((items = [((OCHTTPDAVRequest *)request) responseItemsForBasePath:endpointURL.path drives:_drives reuseUsersByID:self->_usersByUserID driveID:nil withErrors:&errors]) != nil) + { + event.result = items; + } + else + { + event.error = errors.firstObject; + } + } + else + { + // WebDAV root could not be generated (likely due to lack of username) + event.error = OCError(OCErrorInternal); + } + } + else + { + event.error = request.httpResponse.status.error; + } + } + } + + if (event != nil) + { + OCErrorAddDateFromResponse(event.error, request.httpResponse); + [request.eventTarget handleEvent:event sender:self]; + } +} + + +@end diff --git a/ownCloudSDK/Core/Search/OCCore+Search.h b/ownCloudSDK/Core/Search/OCCore+Search.h new file mode 100644 index 00000000..eae6a376 --- /dev/null +++ b/ownCloudSDK/Core/Search/OCCore+Search.h @@ -0,0 +1,30 @@ +// +// OCCore+Search.h +// ownCloudSDK +// +// Created by Felix Schwarz on 31.10.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCCore.h" +#import "OCSearchResult.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCCore (Search) + +- (nullable OCSearchResult *)searchFilesWithPattern:(NSString *)pattern limit:(nullable NSNumber *)limit options:(nullable NSDictionary *)options; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Core/Search/OCCore+Search.m b/ownCloudSDK/Core/Search/OCCore+Search.m new file mode 100644 index 00000000..42f0e044 --- /dev/null +++ b/ownCloudSDK/Core/Search/OCCore+Search.m @@ -0,0 +1,45 @@ +// +// OCCore+Search.m +// ownCloudSDK +// +// Created by Felix Schwarz on 31.10.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCCore+Search.h" +#import "OCConnection.h" +#import "NSError+OCError.h" + +@implementation OCCore (Search) + +- (nullable OCSearchResult *)searchFilesWithPattern:(NSString *)pattern limit:(nullable NSNumber *)limit options:(nullable NSDictionary *)options +{ + OCSearchResult *searchResult = [[OCSearchResult alloc] initWithKQLQuery:pattern core:self]; + + if (self.connectionStatus != OCCoreConnectionStatusOnline) + { + searchResult.error = OCError(OCErrorNotAvailableOffline); + } + else + { + OCProgress *progress = [self.connection searchFilesWithPattern:pattern limit:limit options:options resultTarget:[OCEventTarget eventTargetWithEphermalEventHandlerBlock:^(OCEvent * _Nonnull event, id _Nonnull sender) { + [searchResult _handleResultEvent:event]; + } userInfo:nil ephermalUserInfo:nil]]; + + searchResult.progress = progress; + } + + return (searchResult); +} + +@end diff --git a/ownCloudSDK/Core/Search/OCSearchResult.h b/ownCloudSDK/Core/Search/OCSearchResult.h new file mode 100644 index 00000000..5d87f837 --- /dev/null +++ b/ownCloudSDK/Core/Search/OCSearchResult.h @@ -0,0 +1,47 @@ +// +// OCSearchResult.h +// ownCloudSDK +// +// Created by Felix Schwarz on 31.10.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCDataSource.h" +#import "OCTypes.h" +#import "OCProgress.h" +#import "OCEvent.h" +#import "OCCore.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCSearchResult : NSObject + +@property(weak,nullable) OCCore *core; + +@property(strong) OCKQLQuery kqlQuery; +@property(strong) OCDataSource *results; + +@property(strong,nullable) NSError *error; + +@property(strong,nullable) OCProgress *progress; +- (void)cancel; + +- (instancetype)initWithKQLQuery:(OCKQLQuery)kqlQuery core:(OCCore *)core; + +// MARK: - Internals +- (void)_handleResultEvent:(OCEvent *)event; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Core/Search/OCSearchResult.m b/ownCloudSDK/Core/Search/OCSearchResult.m new file mode 100644 index 00000000..bfa28d96 --- /dev/null +++ b/ownCloudSDK/Core/Search/OCSearchResult.m @@ -0,0 +1,113 @@ +// +// OCSearchResult.m +// ownCloudSDK +// +// Created by Felix Schwarz on 31.10.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCSearchResult.h" +#import "OCDataSourceArray.h" +#import "OCMacros.h" +#import "OCLocation.h" +#import "OCItem+OCDataItem.h" + +@implementation OCSearchResult +{ + NSMapTable *_itemTrackingByLocation; + NSMutableArray *_resultItems; +} + +- (instancetype)initWithKQLQuery:(OCKQLQuery)kqlQuery core:(OCCore *)core +{ + if ((self = [super init]) != nil) + { + _itemTrackingByLocation = [NSMapTable strongToStrongObjectsMapTable]; + _resultItems = [NSMutableArray new]; + + self.core = core; + self.kqlQuery = kqlQuery; + self.results = [[OCDataSourceArray alloc] initWithItems:nil]; + [((OCDataSourceArray *)self.results) setVersionedItems:@[]]; + self.results.state = OCDataSourceStateLoading; + } + + return (self); +} + +- (void)dealloc +{ + if (!self.progress.cancelled) + { + [self.progress cancel]; + } +} + +- (void)cancel +{ + [self.progress cancel]; +} + +- (void)_handleResultEvent:(OCEvent *)event +{ + NSArray *searchResults = OCTypedCast(event.result, NSArray); + OCDataSourceArray *searchResultDatasource = (OCDataSourceArray *)self.results; + + self.results.state = OCDataSourceStateIdle; + + if (event.error != nil) { + self.error = event.error; + OCLogError(@"Search ended with error: %@", event.error); + return; + } + + if (searchResults != nil) + { + for (OCItem *item in searchResults) + { + OCDriveID itemDriveID = item.driveID; + OCPath itemPath = item.path; + + if ((itemDriveID != nil) && (itemPath != nil)) + { + OCLocation *location = [[OCLocation alloc] initWithDriveID:itemDriveID path:itemPath]; + OCCoreItemTracking itemTracker = [self.core trackItemAtLocation:location trackingHandler:^(NSError * _Nullable error, OCItem * _Nullable item, BOOL isInitial) { + if (item != nil) + { + @synchronized(self) { + [self->_resultItems addObject:item]; + [searchResultDatasource setVersionedItems:self->_resultItems]; + + OCLogDebug(@"Result Items: %@", self->_resultItems); + } + } + + if ((error != nil) || (item != nil)) + { + // Stop tracking item when there's an error or the item has been located + @synchronized(self) { + [self->_itemTrackingByLocation removeObjectForKey:location]; + } + } + }]; + + // Keep tracking alive until it delivers a result + @synchronized(self) { + [_itemTrackingByLocation setObject:itemTracker forKey:location]; + } + } + } + } +} + +@end diff --git a/ownCloudSDK/Events/OCEvent.h b/ownCloudSDK/Events/OCEvent.h index 4c36273a..f60c4435 100644 --- a/ownCloudSDK/Events/OCEvent.h +++ b/ownCloudSDK/Events/OCEvent.h @@ -58,7 +58,10 @@ typedef NS_ENUM(NSUInteger, OCEventType) OCEventTypeFilterFiles, // Wakeup - OCEventTypeWakeupSyncRecord + OCEventTypeWakeupSyncRecord, + + // Search + OCEventTypeSearch } __attribute__((enum_extensibility(closed))); @class OCEvent; diff --git a/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.h b/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.h index 0e73cebf..b309aa34 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.h +++ b/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.h @@ -51,7 +51,7 @@ typedef NS_ENUM(NSInteger, OCPropfindDepth) { - (OCXMLNode *)xmlRequestPropAttribute; -- (NSArray *)responseItemsForBasePath:(NSString *)basePath reuseUsersByID:(NSMutableDictionary *)usersByUserID driveID:(OCDriveID)driveID withErrors:(NSArray **)errors; +- (NSArray *)responseItemsForBasePath:(NSString *)basePath drives:(NSArray *)drives reuseUsersByID:(NSMutableDictionary *)usersByUserID driveID:(OCDriveID)driveID withErrors:(NSArray **)errors; - (NSDictionary *)multistatusResponsesForBasePath:(NSString *)basePath; @end diff --git a/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.m b/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.m index c4c25e81..ac787e2f 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.m +++ b/ownCloudSDK/HTTP/Request/OCHTTPDAVRequest.m @@ -91,7 +91,7 @@ - (NSData *)bodyData return (_bodyData); } -- (NSArray *)responseItemsForBasePath:(NSString *)basePath reuseUsersByID:(NSMutableDictionary *)usersByUserID driveID:(nullable OCDriveID)driveID withErrors:(NSArray **)errors +- (NSArray *)responseItemsForBasePath:(NSString *)basePath drives:(NSArray *)drives reuseUsersByID:(NSMutableDictionary *)usersByUserID driveID:(nullable OCDriveID)driveID withErrors:(NSArray **)errors { NSArray *responseItems = nil; NSData *responseData = self.httpResponse.bodyData; @@ -111,10 +111,27 @@ - (NSData *)bodyData { if (basePath != nil) { - parser.options = [NSMutableDictionary dictionaryWithObjectsAndKeys: - basePath, @"basePath", - usersByUserID, @"usersByUserID", - nil]; + NSMutableDictionary *options = [NSMutableDictionary new]; + NSMutableDictionary *drivePrefixMap = nil; + + if (drives != nil) + { + drivePrefixMap = [NSMutableDictionary new]; + for (OCDrive *drive in drives) + { + if (drive.specialType != nil) + { + NSString *drivePrefixPath = [[NSString alloc] initWithFormat:@"/%@/", drive.identifier]; + drivePrefixMap[drivePrefixPath] = drive.identifier; + } + } + } + + options[@"basePath"] = basePath; + options[@"usersByUserID"] = usersByUserID; + options[@"drivePrefixMap"] = drivePrefixMap; + + parser.options = options; } [parser addObjectCreationClasses:@[ [OCItem class], [NSError class] ]]; diff --git a/ownCloudSDK/Item/OCItem+OCTypeAlias.m b/ownCloudSDK/Item/OCItem+OCTypeAlias.m index 5ee4db18..ae8f8332 100644 --- a/ownCloudSDK/Item/OCItem+OCTypeAlias.m +++ b/ownCloudSDK/Item/OCItem+OCTypeAlias.m @@ -82,6 +82,7 @@ @implementation OCItem (MIMETypeAliases) @"application/vnd.visio": @"x-office/document", @"application/vnd.wordperfect": @"x-office/document", @"application/x-7z-compressed": @"package/x-generic", + @"application/x-bzip": @"package/x-generic", @"application/x-bzip2": @"package/x-generic", @"application/x-cbr": @"text", @"application/x-compressed": @"package/x-generic", @@ -95,6 +96,7 @@ @implementation OCItem (MIMETypeAliases) @"application/x-php": @"text/code", @"application/x-rar-compressed": @"package/x-generic", @"application/x-tar": @"package/x-generic", + @"application/x-tgz": @"package/x-generic", @"application/x-tex": @"text", @"application/xml": @"text/html", @"application/yaml": @"text/code", diff --git a/ownCloudSDK/Item/OCItem+OCXMLObjectCreation.m b/ownCloudSDK/Item/OCItem+OCXMLObjectCreation.m index 86cad821..0a08eb28 100644 --- a/ownCloudSDK/Item/OCItem+OCXMLObjectCreation.m +++ b/ownCloudSDK/Item/OCItem+OCXMLObjectCreation.m @@ -20,6 +20,7 @@ #import "OCItem+OCXMLObjectCreation.h" #import "OCHTTPStatus.h" #import "OCChecksum.h" +#import "OCDrive.h" @implementation OCItem (OCXMLObjectCreation) @@ -79,6 +80,13 @@ + (OCXMLParserNodeKeyValueEnumeratorDictionary)_sharedKeyValueEnumeratorDict } } copy], + @"oc:fileid" : [^(OCItem *item, NSString *key, id value) { // Returned in "oc:search-files" REPORT responses only (2024-10-31), identical to "oc:id" + if ([value isKindOfClass:[NSString class]]) + { + item.fileID = value; + } + } copy], + @"oc:permissions" : [^(OCItem *item, NSString *key, id value) { if ([value isKindOfClass:[NSString class]]) { @@ -219,6 +227,13 @@ + (OCXMLParserNodeKeyValueEnumeratorDictionary)_sharedKeyValueEnumeratorDict } } copy], + @"oc:score": [^(OCItem *item, NSString *key, id value) { + if ([value isKindOfClass:NSString.class]) + { + item.searchScore = @(((NSString *)value).doubleValue); + } + } copy], + @"d:quota-available-bytes" : [^(OCItem *item, NSString *key, id value) { if ([value isKindOfClass:[NSString class]]) { @@ -258,6 +273,7 @@ + (instancetype)instanceFromNode:(OCXMLParserNode *)responseNode xmlParser:(OCXM if ((item = [OCItem new]) != nil) { OCPath basePath; + NSDictionary *drivePrefixMap; // Remove base path (if applicable) if ((basePath = xmlParser.options[@"basePath"]) != nil) @@ -268,6 +284,20 @@ + (instancetype)instanceFromNode:(OCXMLParserNode *)responseNode xmlParser:(OCXM } } + if ((drivePrefixMap = xmlParser.options[@"drivePrefixMap"]) != nil) + { + // Remove drive base path (for PROPFINDs returning items from different drives) + // and pre-assign driveID (in case it's missing in the response) + for (NSString *drivePrefixPath in drivePrefixMap) + { + if ([itemPath hasPrefix:drivePrefixPath]) + { + itemPath = [itemPath substringFromIndex:drivePrefixPath.length-1]; + item.driveID = drivePrefixMap[drivePrefixPath]; + } + } + } + item.path = itemPath; // Extract Properties diff --git a/ownCloudSDK/Item/OCItem.h b/ownCloudSDK/Item/OCItem.h index 90109e17..79b158fe 100644 --- a/ownCloudSDK/Item/OCItem.h +++ b/ownCloudSDK/Item/OCItem.h @@ -188,6 +188,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable,strong) NSNumber *quotaBytesRemaining; //!< Remaining space (if a quota is set) @property(nullable,strong) NSNumber *quotaBytesUsed; //!< Used space (if a quota is set) +@property(nullable,strong) NSNumber *searchScore; //!< Score returned by server-side search (for server-side searches only) (dynamic/ephermal) + @property(readonly,nonatomic) BOOL compactingAllowed; //!< YES if the local copy may be removed during compacting. @property(assign) OCItemVersionSeed versionSeed; //!< Version seed that changes whenever the item is updated diff --git a/ownCloudSDK/OCTypes.h b/ownCloudSDK/OCTypes.h index 3a7248b5..3832c78a 100644 --- a/ownCloudSDK/OCTypes.h +++ b/ownCloudSDK/OCTypes.h @@ -69,4 +69,6 @@ typedef NSMutableDictionary>* OCMutableCodableDict; typedef NSString* OCActionTrackingID; //!< Identifier used to track a triggered action's progress / state +typedef NSString *OCKQLQuery; //!< KQL query string + #endif /* OCTypes_h */ diff --git a/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.h b/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.h new file mode 100644 index 00000000..2323f739 --- /dev/null +++ b/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.h @@ -0,0 +1,36 @@ +// +// OCQueryCondition+KQLBuilder.h +// ownCloudSDK +// +// Created by Felix Schwarz on 19.11.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCQueryCondition.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* OCKQLString; + +typedef NS_OPTIONS(NSInteger, OCKQLSearchedContent) { + OCKQLSearchedContentItemName = (1L << 0L), + OCKQLSearchedContentContents = (1L << 1L) +}; + +@interface OCQueryCondition (KQLBuilder) + +- (OCKQLString)kqlStringWithTypeAliasToKQLTypeMap:(NSDictionary *)typeAliasToKQLTypeMap targetContent:(OCKQLSearchedContent)targetContent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.m b/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.m new file mode 100644 index 00000000..e6ef8626 --- /dev/null +++ b/ownCloudSDK/Query/Condition/OCQueryCondition+KQLBuilder.m @@ -0,0 +1,276 @@ +// +// OCQueryCondition+KQLBuilder.m +// ownCloudSDK +// +// Created by Felix Schwarz on 19.11.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCQueryCondition+KQLBuilder.h" +#import "OCLogger.h" +#import "OCMacros.h" +#import "NSString+OCSQLTools.h" + +@interface NSString (KQLTools) +@end + +@implementation NSString (KQLTools) + +- (NSString *)stringByKQLEscaping +{ + return ([NSString stringWithFormat:@"\"%@\"", [self stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]]); +} + +@end + +@interface NSDate (KQLTools) +@end + +@implementation NSDate (KQLTools) + +- (NSString *)kqlRFC3339DateString +{ + NSDateFormatter *rfc3339DateFormatter; + NSString *str; + + rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + rfc3339DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + rfc3339DateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + rfc3339DateFormatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ"; + + str = [rfc3339DateFormatter stringFromDate:self]; + + /* + Unfortunately, NSDateFormatter produces "0000" instead of "00:00" for time zones, mismatching what is otherwise widely used + and failing conversion by golang's time.Parse(time.RFC3339, ..). We therefore have to account for this for as long as it is + that way and insert ":" in the timezone part *if needed* ("2024-11-17T23:00:00+0000" => "2024-11-17T23:00:00+00:00") + to be compatible with golang's time.RFC3339 + (as used in https://github.com/owncloud/ocis/blob/cff364c998355b1295793e9244e5efdfea064536/services/search/pkg/engine/bleve.go#L225) + + This can be verified with this golang snippet: + package main + + import ( + "fmt" + "time" + ) + + func main() { + tOk, err := time.Parse(time.RFC3339, "2024-11-17T23:00:00+01:00") + fmt.Printf("parsed as: %v (error: %v)\n", tOk, err) + + tFail, err := time.Parse(time.RFC3339, "2024-11-17T23:00:00+0100") + fmt.Printf("parsed as: %v (error: %v)\n", tFail, err) + } + + produces: + parsed as: 2024-11-17 23:00:00 +0100 +0100 (error: ) + parsed as: 0001-01-01 00:00:00 +0000 UTC (error: parsing time "2024-11-17T23:00:00+0100" as "2006-01-02T15:04:05Z07:00": cannot parse "+0100" as "Z07:00") + */ + NSRange timezoneDelimiterPosition = [str rangeOfString:@"+" options:NSBackwardsSearch]; + if (timezoneDelimiterPosition.location == (str.length-5)) { // No ":" separating the timezone hours and minutes => add it + str = [NSString stringWithFormat:@"%@:%@", [str substringToIndex:str.length-2], [str substringFromIndex:str.length-2]]; + } + + return (str); +} + +@end + +@implementation OCQueryCondition (KQLBuilder) + +- (OCKQLString)kqlStringWithTypeAliasToKQLTypeMap:(NSDictionary *)typeAliasToKQLTypeMap targetContent:(OCKQLSearchedContent)targetContent +{ + // Mappings as per as per https://github.com/owncloud/ocis/blob/21893235442194f790f8c369f3b695eba79a43b9/services/search/pkg/query/bleve/compiler.go#L13 + NSString *kqlString = [self _kqlStringWithColumnNameMap:@{ + OCItemPropertyNameName: @"name", + OCItemPropertyNameLastModified: @"mtime", + OCItemPropertyNameLastUsed: @"mtime", // not an exact match, but closest possible + OCItemPropertyNameMIMEType: @"mediatype", + OCItemPropertyNameTypeAlias: @"mediatype", // conversion from local typeAlias to server-side alias taking place via typeAliasToKQLTypeMap + OCItemPropertyNameType: @"mediatype", // conversion from OCItemType taking place in -kqlStringWithColumnNameMap:typeAliasToKQLTypeMap: + OCItemPropertyNameSize: @"size", + } typeAliasToKQLTypeMap:typeAliasToKQLTypeMap targetContent:targetContent]; + + return (kqlString); +} + +- (NSString *)_convert:(id)obj smartQuote:(BOOL)smartQuote // smartQuote -> only quote types where it's possible and necessary, f.ex. don't quote numbers +{ + NSString *str = nil; + + if ([obj isKindOfClass:NSString.class]) { + str = smartQuote ? [(NSString *)obj stringByKQLEscaping] : obj; + } + + if ([obj isKindOfClass:NSNumber.class]) { + str = ((NSNumber *)obj).stringValue; + } + + if ([obj isKindOfClass:NSDate.class]) { + str = ((NSDate *)obj).kqlRFC3339DateString; + } + + return (str); +} + +- (NSString *)_convertedValue +{ + return [self _convert:self.value smartQuote:YES]; +} + +- (OCKQLString)_kqlStringWithColumnNameMap:(NSDictionary *)propertyColumnNameMap typeAliasToKQLTypeMap:(NSDictionary *)typeAliasToKQLTypeMap targetContent:(OCKQLSearchedContent)targetContent +{ + // Example "(name:"*ex*" OR content:"ex") AND mtime:last 30 days AND mediatype:("pdf" OR "presentation")" + NSString *query = nil; + NSString *kqlProperty = propertyColumnNameMap[self.property]; + NSString *unquotedValue = nil; + NSString *smartQuotedValue = nil; + + if ([self.property isEqual:OCItemPropertyNameTypeAlias]) { + // Convert type aliases to KQL types + unquotedValue = typeAliasToKQLTypeMap[self.value]; + } + + if ([self.property isEqual:OCItemPropertyNameType]) { + // Convert type to mime type + if ([self.value isEqual:@(OCItemTypeFile)]) { + unquotedValue = @"file"; + } + if ([self.value isEqual:@(OCItemTypeCollection)]) { + unquotedValue = @"folder"; + } + } + + if (unquotedValue == nil) { + // No pre-converted value -> use standard conversion + unquotedValue = [self _convert:self.value smartQuote:NO]; + smartQuotedValue = [self _convert:self.value smartQuote:YES]; + } else if (smartQuotedValue == nil) { + // No pre-quoted value -> use standard escaping + smartQuotedValue = [unquotedValue stringByKQLEscaping]; + } + + switch (self.operator) + { + case OCQueryConditionOperatorPropertyGreaterThanValue: + query = [[NSString alloc] initWithFormat:@"(%@:>%@)", kqlProperty, smartQuotedValue]; + break; + + case OCQueryConditionOperatorPropertyLessThanValue: + query = [[NSString alloc] initWithFormat:@"(%@:<%@)", kqlProperty, smartQuotedValue]; + break; + + case OCQueryConditionOperatorPropertyEqualToValue: + query = [[NSString alloc] initWithFormat:@"(%@:%@)", kqlProperty, smartQuotedValue]; + break; + + case OCQueryConditionOperatorPropertyNotEqualToValue: + query = [[NSString alloc] initWithFormat:@"(%@<>%@)", kqlProperty, smartQuotedValue]; + break; + + case OCQueryConditionOperatorPropertyHasPrefix: + query = [[NSString alloc] initWithFormat:@"(%@:%@)", kqlProperty, [[unquotedValue stringByAppendingString:@"*"] stringByKQLEscaping]]; + break; + + case OCQueryConditionOperatorPropertyHasSuffix: + query = [[NSString alloc] initWithFormat:@"(%@:%@)", kqlProperty, [[@"*" stringByAppendingString:unquotedValue] stringByKQLEscaping]]; + break; + + case OCQueryConditionOperatorPropertyContains: { + NSString *kqlValue = [[NSString stringWithFormat:@"*%@*", unquotedValue] stringByKQLEscaping]; + if ([self.property isEqual:OCItemPropertyNameName]) { + if (targetContent & OCKQLSearchedContentItemName) { + query = [[NSString alloc] initWithFormat:@"(name:%@)", kqlValue]; + } + + if (targetContent & OCKQLSearchedContentContents) { + NSString *contentQuery = [[NSString alloc] initWithFormat:@"(content:%@)", kqlValue]; + + if (query == nil) { + query = contentQuery; + } else { + query = [NSString stringWithFormat:@"(%@ OR %@)", query, contentQuery]; + } + } + + break; + } + query = [[NSString alloc] initWithFormat:@"(%@:%@)", kqlProperty, kqlValue]; + } + break; + + case OCQueryConditionOperatorAnd: + case OCQueryConditionOperatorOr: { + NSArray *conditions; + + if ((conditions = OCTypedCast(self.value, NSArray)) != nil) + { + NSMutableString *queryString = [NSMutableString new]; + NSString *operatorString = (self.operator == OCQueryConditionOperatorAnd) ? @"AND" : @"OR"; + + if (conditions.count == 1) + { + // Simplify case where there is only one condition + query = [conditions.firstObject _kqlStringWithColumnNameMap:propertyColumnNameMap typeAliasToKQLTypeMap:typeAliasToKQLTypeMap targetContent:targetContent]; + } + else + { + for (OCQueryCondition *condition in conditions) + { + NSString *conditionQueryString = nil; + + if ((conditionQueryString = [condition _kqlStringWithColumnNameMap:propertyColumnNameMap typeAliasToKQLTypeMap:typeAliasToKQLTypeMap targetContent:targetContent]) != nil) + { + if (queryString.length > 0) + { + [queryString appendFormat:@" %@ %@", operatorString, conditionQueryString]; + } + else + { + [queryString appendString:conditionQueryString]; + } + } + } + + query = [NSString stringWithFormat:@"(%@)", queryString]; + } + } + else + { + OCLogError(@"AND/OR condition value is not an array of conditions"); + } + } + break; + + case OCQueryConditionOperatorNegate: { + OCQueryCondition *condition; + + if ((condition = OCTypedCast(self.value, OCQueryCondition)) != nil) + { + query = [NSString stringWithFormat:@"(NOT %@)", [condition _kqlStringWithColumnNameMap:propertyColumnNameMap typeAliasToKQLTypeMap:typeAliasToKQLTypeMap targetContent:targetContent]]; + } + else + { + OCLogError(@"Negate condition value is not a condition"); + } + } + break; + } + + // self.sortBy and self.maxResultCount are ignored for now + + return (query); +} + +@end diff --git a/ownCloudSDK/ownCloudSDK.h b/ownCloudSDK/ownCloudSDK.h index 45c58c86..50e3e9cf 100644 --- a/ownCloudSDK/ownCloudSDK.h +++ b/ownCloudSDK/ownCloudSDK.h @@ -76,6 +76,8 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import #import +#import +#import #import #import #import @@ -228,6 +230,7 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import #import +#import #import #import