diff --git a/Brand/Database.swift b/Brand/Database.swift index fe82386fed..59ec61a758 100644 --- a/Brand/Database.swift +++ b/Brand/Database.swift @@ -26,4 +26,4 @@ import Foundation // Database Realm // let databaseName = "nextcloud.realm" -let databaseSchemaVersion: UInt64 = 367 +let databaseSchemaVersion: UInt64 = 368 diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 6b885680d1..ffdc51bb9d 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -16,6 +16,18 @@ 371B5A2E23D0B04500FAFAE9 /* NCMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */; }; 3781B9B023DB2B7E006B4B1D /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3781B9AF23DB2B7E006B4B1D /* AppDelegate+Menu.swift */; }; 8491B1CD273BBA82001C8C5B /* UIViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8491B1CC273BBA82001C8C5B /* UIViewController+Menu.swift */; }; + AA3494FE2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3494FF2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495002CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495012CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495022CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495032CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495042CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AAAC0A122CEE34700001949E /* NCShareDownloadLimitNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */; }; + AAF806B22CE25E67009C2D43 /* NCShareDateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */; }; + AAF806B42CE25EFF009C2D43 /* NCShareToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */; }; + AAF806B62CE34C7A009C2D43 /* NCShareDownloadLimitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */; }; + AAF806B82CE37C1A009C2D43 /* NCShareDownloadLimitTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -1152,6 +1164,13 @@ 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMenu.swift; sourceTree = ""; }; 3781B9AF23DB2B7E006B4B1D /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; }; 8491B1CC273BBA82001C8C5B /* UIViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Menu.swift"; sourceTree = ""; }; + AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+DownloadLimit.swift"; sourceTree = ""; }; + AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitNetworking.swift; sourceTree = ""; }; + AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDateCell.swift; sourceTree = ""; }; + AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareToggleCell.swift; sourceTree = ""; }; + AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitViewController.swift; sourceTree = ""; }; + AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewController.swift; sourceTree = ""; }; + AAF806B92CE38BB2009C2D43 /* NextcloudKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NextcloudKit; path = ../NextcloudKit; sourceTree = SOURCE_ROOT; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -1921,6 +1940,15 @@ path = Menu; sourceTree = ""; }; + AA3494FC2CE4FF02005CC075 /* DownloadLimit */ = { + isa = PBXGroup; + children = ( + AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */, + AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */, + ); + path = DownloadLimit; + sourceTree = ""; + }; AF8ED1FA2757821000B8DBC4 /* NextcloudUnitTests */ = { isa = PBXGroup; children = ( @@ -1932,11 +1960,14 @@ AF93471327E235EB002537EE /* Advanced */ = { isa = PBXGroup; children = ( + AA3494FC2CE4FF02005CC075 /* DownloadLimit */, AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */, AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */, AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */, AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */, + AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */, AF93474D27E3F211002537EE /* NCShareNewUserAddComment.swift */, + AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */, ); path = Advanced; sourceTree = ""; @@ -2148,6 +2179,7 @@ F787704E22E7019900F287A9 /* NCShareLinkCell.xib */, AF2D7C7B2742556F00ADF566 /* NCShareLinkCell.swift */, F769454722E9F20D000A798A /* NCShareNetworking.swift */, + AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */, F769453F22E9F077000A798A /* NCSharePaging.swift */, F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */, F769453B22E9CFFF000A798A /* NCShareUserCell.xib */, @@ -2678,6 +2710,7 @@ F7BAAD951ED5A63D00B7EAD4 /* Data */ = { isa = PBXGroup; children = ( + AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */, F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */, AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */, AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */, @@ -3014,6 +3047,7 @@ F7F67B9F1A24D27800EE80DA = { isa = PBXGroup; children = ( + AAF806B92CE38BB2009C2D43 /* NextcloudKit */, F7B8B82F25681C3400967775 /* GoogleService-Info.plist */, F7C1CDD91E6DFC6F005D92BE /* Brand */, F7F67BAA1A24D27800EE80DA /* iOSClient */, @@ -3843,6 +3877,7 @@ F711A4E22AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F7401C1B2C75E6F300649E87 /* NCCapabilities.swift in Sources */, AF4BF61C27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */, + AA3495022CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F78E2D6B29AF02DB0024D4F3 /* Database.swift in Sources */, F7817CFF29802D1A00FFBC65 /* NCPushNotificationEncryption.m in Sources */, F798F0EC2588060A000DAFFD /* UIColor+Extension.swift in Sources */, @@ -3915,6 +3950,7 @@ F711A4E12AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F76882382C0DD22F001CF441 /* NCKeychain.swift in Sources */, F7C9B9222B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, + AA3495032CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7490E8529882C8C009DCE94 /* NCManageDatabase+Video.swift in Sources */, F7490E7729882C10009DCE94 /* UIColor+Extension.swift in Sources */, F70716E62987F81500E72C1D /* DocumentActionViewController.swift in Sources */, @@ -3963,6 +3999,7 @@ F73EF7DA2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F7817CFB29801A3500FFBC65 /* Data+Extension.swift in Sources */, F72429362AFE39860040AEF3 /* NCLivePhoto.swift in Sources */, + AA3494FF2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, AF4BF61F27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, F7CBC1262BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, F7A0D1362591FBC5008F8A13 /* String+Extension.swift in Sources */, @@ -4085,6 +4122,7 @@ F783030328B4C4DD00B84583 /* ThreadSafeDictionary.swift in Sources */, F77ED59128C9CE9D00E24ED0 /* ToolbarData.swift in Sources */, F78302F728B4C3C900B84583 /* NCManageDatabase.swift in Sources */, + AA3495042CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F359D8682A7D03420023F405 /* NCUtility+Exif.swift in Sources */, F7346E1628B0EF5C006CE2D2 /* Widget.swift in Sources */, F78302F828B4C3E100B84583 /* NCManageDatabase+Activity.swift in Sources */, @@ -4217,6 +4255,7 @@ F7327E392B73B8D400A462C7 /* Array+Extension.swift in Sources */, F78E2D6929AF02DB0024D4F3 /* Database.swift in Sources */, F749B64E297B0CBB00087535 /* NCManageDatabase+Share.swift in Sources */, + AA3494FE2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F73EF7B32B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, F7401C192C75E6F300649E87 /* NCCapabilities.swift in Sources */, F771E3F320E239A600AFB62D /* FileProviderData.swift in Sources */, @@ -4329,6 +4368,7 @@ F758B460212C56A400515F55 /* NCScan.swift in Sources */, F76882262C0DD1E7001CF441 /* NCSettingsView.swift in Sources */, F78ACD52219046DC0088454D /* NCSectionFirstHeader.swift in Sources */, + AAF806B42CE25EFF009C2D43 /* NCShareToggleCell.swift in Sources */, F72944F52A8424F800246839 /* NCEndToEndMetadataV1.swift in Sources */, F710D2022405826100A6033D /* NCViewer+Menu.swift in Sources */, F765E9CD295C585800A09ED8 /* NCUploadScanDocument.swift in Sources */, @@ -4366,6 +4406,7 @@ F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + AA3495012CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, @@ -4407,6 +4448,7 @@ F7EB9B132BBC12F300EDF036 /* UIApplication+Extension.swift in Sources */, F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */, F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */, + AAF806B62CE34C7A009C2D43 /* NCShareDownloadLimitViewController.swift in Sources */, F76882222C0DD1E7001CF441 /* NCCapabilitiesView.swift in Sources */, AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */, F7F878AE1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, @@ -4494,6 +4536,7 @@ F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, F70968A424212C4E00ED60E5 /* NCLivePhoto.swift in Sources */, + AAF806B22CE25E67009C2D43 /* NCShareDateCell.swift in Sources */, F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */, F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */, F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnail.swift in Sources */, @@ -4515,8 +4558,10 @@ F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, F799DF8B2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift in Sources */, + AAAC0A122CEE34700001949E /* NCShareDownloadLimitNetworking.swift in Sources */, F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */, F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */, + AAF806B82CE37C1A009C2D43 /* NCShareDownloadLimitTableViewController.swift in Sources */, F76882242C0DD1E7001CF441 /* NCSettingsAdvancedView.swift in Sources */, F75CA1472962F13700B01130 /* NCHUDView.swift in Sources */, F77BB748289985270090FC19 /* UITabBarController+Extension.swift in Sources */, @@ -4558,6 +4603,7 @@ F7C9739528F17131002C43E2 /* IntentHandler.swift in Sources */, F7A8D73D28F181D3008BBE1C /* NCUtilityFileSystem.swift in Sources */, F73EF7E12B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, + AA3495002CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7C9B91F2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, F75DD767290ABB25002EB562 /* Intent.intentdefinition in Sources */, F72437812C10B92500C7C68D /* NCPermissions.swift in Sources */, diff --git a/iOSClient/Data/NCManageDatabase+Capabilities.swift b/iOSClient/Data/NCManageDatabase+Capabilities.swift index 059847c1fa..d983e61398 100644 --- a/iOSClient/Data/NCManageDatabase+Capabilities.swift +++ b/iOSClient/Data/NCManageDatabase+Capabilities.swift @@ -88,6 +88,7 @@ extension NCManageDatabase { } struct Capabilities: Codable { + let downloadLimit: DownloadLimit? let filessharing: FilesSharing? let theming: Theming? let endtoendencryption: EndToEndEncryption? @@ -102,6 +103,7 @@ extension NCManageDatabase { let assistant: Assistant? enum CodingKeys: String, CodingKey { + case downloadLimit = "downloadlimit" case filessharing = "files_sharing" case theming case endtoendencryption = "end-to-end-encryption" @@ -112,6 +114,11 @@ extension NCManageDatabase { case assistant } + struct DownloadLimit: Codable { + let enabled: Bool? + let defaultLimit: Int? + } + struct FilesSharing: Codable { let apienabled: Bool? let groupsharing: Bool? @@ -327,6 +334,8 @@ extension NCManageDatabase { capabilities.capabilityFileSharingInternalExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 capabilities.capabilityFileSharingRemoteExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateremote?.enforced ?? false capabilities.capabilityFileSharingRemoteExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateremote?.days ?? 0 + capabilities.capabilityFileSharingDownloadLimit = data.capabilities.downloadLimit?.enabled ?? false + capabilities.capabilityFileSharingDownloadLimitDefaultLimit = data.capabilities.downloadLimit?.defaultLimit ?? 1 capabilities.capabilityThemingColor = data.capabilities.theming?.color ?? "" capabilities.capabilityThemingColorElement = data.capabilities.theming?.colorelement ?? "" diff --git a/iOSClient/Data/NCManageDatabase+DownloadLimit.swift b/iOSClient/Data/NCManageDatabase+DownloadLimit.swift new file mode 100644 index 0000000000..300ea10b12 --- /dev/null +++ b/iOSClient/Data/NCManageDatabase+DownloadLimit.swift @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit +import RealmSwift + +/// +/// Data model for storing information about download limits of shares. +/// +class tableDownloadLimit: Object { + /// + /// The number of downloads which already happened. + /// + @Persisted + @objc dynamic var count: Int = 0 + + /// + /// Total number of allowed downloas. + /// + @Persisted + @objc dynamic var limit: Int = 0 + + /// + /// The token identifying the related share. + /// + @Persisted(primaryKey: true) + @objc dynamic var token: String = "" +} + +extension NCManageDatabase { + /// + /// Create a new download limit object in the database. + /// + @discardableResult + func createDownloadLimit(count: Int, limit: Int, token: String) throws -> tableDownloadLimit? { + let downloadLimit = tableDownloadLimit() + downloadLimit.count = count + downloadLimit.limit = limit + downloadLimit.token = token + + do { + let realm = try Realm() + + try realm.write { + realm.add(downloadLimit, update: .all) + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + + return downloadLimit + } + + /// + /// Delete an existing download limit object identified by the token of its related share. + /// + /// - Parameter token: The `token` of the associated ``Nextcloud/tableShare/token``. + /// + func deleteDownloadLimit(byShareToken token: String) throws { + do { + let realm = try Realm() + + try realm.write { + let result = realm.objects(tableDownloadLimit.self).filter("token == %@", token) + realm.delete(result) + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + + /// + /// Retrieve a download limit by the token of the associated ``Nextcloud/tableShare/token``. + /// + /// - Parameter token: The `token` of the associated ``tableShare``. + /// + func getDownloadLimit(byShareToken token: String) throws -> tableDownloadLimit? { + do { + let realm = try Realm() + let predicate = NSPredicate(format: "token == %@", token) + + guard let result = realm.objects(tableDownloadLimit.self).filter(predicate).first else { + return nil + } + + return result + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + } + + return nil + } +} diff --git a/iOSClient/Data/NCManageDatabase+Share.swift b/iOSClient/Data/NCManageDatabase+Share.swift index c518597d42..86b2f9a921 100644 --- a/iOSClient/Data/NCManageDatabase+Share.swift +++ b/iOSClient/Data/NCManageDatabase+Share.swift @@ -54,7 +54,12 @@ class tableShareV2: Object { @objc dynamic var primaryKey = "" @objc dynamic var sendPasswordByTalk: Bool = false @objc dynamic var serverUrl = "" + + /// + /// See [OCS Share API documentation](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html) for semantic definitions of the different possible values. + /// @objc dynamic var shareType: Int = 0 + @objc dynamic var shareWith = "" @objc dynamic var shareWithDisplayname = "" @objc dynamic var storage: Int = 0 @@ -146,22 +151,48 @@ extension NCManageDatabase { return [] } + /// + /// Fetch all available shares of an item identified by the given metadata. + /// + /// - Returns: A tuple consisting of the first public share link and any _additional_ shares that might be there. + /// It is possible that there is no public share link but still shares of other types. + /// In the latter case, all shares are returned as the second tuple value. + /// func getTableShares(metadata: tableMetadata) -> (firstShareLink: tableShare?, share: [tableShare]?) { do { let realm = try Realm() realm.refresh() - let sortProperties = [SortDescriptor(keyPath: "shareType", ascending: false), SortDescriptor(keyPath: "idShare", ascending: false)] - let firstShareLink = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName).first + + let sortProperties = [ + SortDescriptor(keyPath: "shareType", ascending: false), + SortDescriptor(keyPath: "idShare", ascending: false) + ] + + let firstShareLink = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName) + .first + if let firstShareLink = firstShareLink { - let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare).sorted(by: sortProperties) - return(firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) })) + let results = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare) + .sorted(by: sortProperties) + + return (firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) })) } else { - let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName).sorted(by: sortProperties) - return(firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) })) + let results = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName) + .sorted(by: sortProperties) + + return (firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) })) } + } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } + return (nil, nil) } @@ -190,6 +221,9 @@ extension NCManageDatabase { return [] } + /// + /// Fetch all shares of a file regardless of type. + /// func getTableShares(account: String, serverUrl: String, fileName: String) -> [tableShare] { do { let realm = try Realm() diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index 2934948612..6e6008ddf5 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -84,7 +84,8 @@ class NCManageDatabase: NSObject { tableDashboardWidget.self, tableDashboardWidgetButton.self, NCDBLayoutForView.self, - TableSecurityGuardDiagnostics.self] + TableSecurityGuardDiagnostics.self, + tableDownloadLimit.self] // Disable file protection for directory DB // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm diff --git a/iOSClient/Menu/NCShare+Menu.swift b/iOSClient/Menu/NCShare+Menu.swift index 62ab9c8271..2770b5e471 100644 --- a/iOSClient/Menu/NCShare+Menu.swift +++ b/iOSClient/Menu/NCShare+Menu.swift @@ -54,6 +54,7 @@ extension NCShare { advancePermission.share = tableShare(value: share) advancePermission.oldTableShare = tableShare(value: share) advancePermission.metadata = self.metadata + advancePermission.downloadLimit = try? self.database.getDownloadLimit(byShareToken: share.token) navigationController.pushViewController(advancePermission, animated: true) } ) diff --git a/iOSClient/NCCapabilities.swift b/iOSClient/NCCapabilities.swift index 8d56106b48..755665c404 100644 --- a/iOSClient/NCCapabilities.swift +++ b/iOSClient/NCCapabilities.swift @@ -46,6 +46,8 @@ public class NCCapabilities: NSObject { var capabilityFileSharingRemoteExpireDateEnforced: Bool = false var capabilityFileSharingRemoteExpireDateDays: Int = 0 var capabilityFileSharingDefaultPermission: Int = 0 + var capabilityFileSharingDownloadLimit: Bool = false + var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 var capabilityThemingColor: String = "" var capabilityThemingColorElement: String = "" var capabilityThemingColorText: String = "" diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 22625208b6..271d1c0848 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -95,6 +95,24 @@ extension NCNetworking { let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file) let metadata = self.database.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) + // Remove all known download limits from shares related to the given file. + // This avoids obsolete download limit objects to stay around. + // Afterwards create new download limits, should any such be returned for the known shares. + + let shares = self.database.getTableShares(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName) + + do { + try shares.forEach { share in + try self.database.deleteDownloadLimit(byShareToken: share.token) + + if let receivedDownloadLimit = file.downloadLimits.first(where: { $0.token == share.token }) { + try self.database.createDownloadLimit(count: receivedDownloadLimit.count, limit: receivedDownloadLimit.limit, token: receivedDownloadLimit.token) + } + } + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not update download limits: \(error)") + } + completion(account, metadata, error) } } diff --git a/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift b/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift index 86c6f43bcb..ef4c169c18 100644 --- a/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift +++ b/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift @@ -10,6 +10,11 @@ import Foundation import UIKit import SwiftUI +/// +/// Data model for ``NCCapabilitiesView``. +/// +/// Compiles capabilities, their availability and symbol images for display. +/// class NCCapabilitiesModel: ObservableObject, ViewOnAppearHandling { struct Capability: Identifiable, Hashable { let id = UUID() @@ -44,6 +49,9 @@ class NCCapabilitiesModel: ObservableObject, ViewOnAppearHandling { var image = utility.loadImage(named: "person.fill.badge.plus") capabililies.append(Capability(text: "File sharing", image: image, resize: false, available: capability.capabilityFileSharingApiEnabled)) + image = utility.loadImage(named: "gauge.with.dots.needle.bottom.100percent") + capabililies.append(Capability(text: "Download Limit", image: image, resize: false, available: capability.capabilityFileSharingDownloadLimit)) + image = utility.loadImage(named: "network") capabililies.append(Capability(text: "External site", image: image, resize: false, available: capability.capabilityExternalSites)) diff --git a/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift new file mode 100644 index 0000000000..a91a675bc2 --- /dev/null +++ b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import NextcloudKit +import UIKit + +/// +/// View controller for the table view managing the input form for download limits. +/// +/// This child view controller is required because table views require a dedicated table view controller. +/// +class NCShareDownloadLimitTableViewController: UITableViewController { + let database = NCManageDatabase.shared + + /// + /// The initial state injected from the parent view controller on appearance. + /// + public var initialDownloadLimit: tableDownloadLimit? + public var metadata: tableMetadata! + public var share: NCTableShareable! + + /// + /// Default value for limits as possibly provided by the server capabilities. + /// + var defaultLimit: Int { + NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityFileSharingDownloadLimitDefaultLimit + } + + /// + /// Share token required to work with download limits. + /// + private var token: String! + + /// + /// The final state to apply once the view is about to disappear. + /// + private var finalDownloadLimit: tableDownloadLimit? + + private var networking: NCShareDownloadLimitNetworking! + + @IBOutlet var limitSwitch: UISwitch! + @IBOutlet var limitTextField: UITextField! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let initialDownloadLimit { + limitSwitch.isOn = true + limitTextField.text = "\(initialDownloadLimit.limit)" + + finalDownloadLimit = tableDownloadLimit() + finalDownloadLimit?.count = initialDownloadLimit.count + finalDownloadLimit?.limit = initialDownloadLimit.limit + finalDownloadLimit?.token = initialDownloadLimit.token + } else { + limitSwitch.isOn = false + } + + if let token = self.database.getTableShare(account: metadata.account, idShare: share.idShare)?.token { + self.token = token + } else { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to resolve share token!") + self.token = "" + } + + networking = NCShareDownloadLimitNetworking(account: metadata.account, delegate: self, token: token) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row == 1 else { + super.tableView(tableView, didSelectRowAt: indexPath) + return + } + + // The accessory text field should become first responder regardless where the user tapped in the table row. + limitTextField.becomeFirstResponder() + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + // Programmatically hide the limit input row depending on limit enablement. + if limitSwitch.isOn == false && indexPath.row == 1 { + return 0 + } + + return super.tableView(tableView, heightForRowAt: indexPath) + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if let finalDownloadLimit { + String(format: NSLocalizedString("_remaining_share_downloads_", comment: "Table footer text for form of configuring download limits."), finalDownloadLimit.limit - finalDownloadLimit.count) + } else { + nil + } + } + + @IBAction func switchDownloadLimit(_ sender: UISwitch) { + if sender.isOn { + finalDownloadLimit = tableDownloadLimit() + finalDownloadLimit?.count = 0 + finalDownloadLimit?.limit = defaultLimit + finalDownloadLimit?.token = token + + limitTextField.text = String(defaultLimit) + } else { + finalDownloadLimit = nil + } + + tableView.reloadData() + dispatchShareDownloadLimitUpdate() + } + + @IBAction func editingAllowedDownloadsDidBegin(_ sender: UITextField) { + sender.selectAll(nil) + } + + @IBAction func editingAllowedDownloadsDidEnd(_ sender: UITextField) { + finalDownloadLimit?.limit = Int(sender.text ?? "1") ?? defaultLimit + finalDownloadLimit?.count = 0 + + tableView.reloadData() + dispatchShareDownloadLimitUpdate() + } + + func dispatchShareDownloadLimitUpdate() { + guard let text = limitTextField.text else { + return + } + + guard let limit = Int(text) else { + return + } + + if limitSwitch.isOn { + networking.setShareDownloadLimit(limit: limit) + } else { + networking.removeShareDownloadLimit() + } + } +} + +// MARK: - NCShareDownloadLimitNetworkingDelegate + +extension NCShareDownloadLimitTableViewController: NCShareDownloadLimitNetworkingDelegate { + func downloadLimitRemoved(by token: String, in account: String) { + do { + try self.database.deleteDownloadLimit(byShareToken: token) + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to delete download limit in database!") + } + } + + func downloadLimitSet(to limit: Int, by token: String, in account: String) { + do { + try self.database.createDownloadLimit(count: 0, limit: limit, token: token) + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to create download limit in database!") + } + } +} diff --git a/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift new file mode 100644 index 0000000000..13e9515353 --- /dev/null +++ b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +/// +/// View controller for the download limit detail view in share details. +/// +class NCShareDownloadLimitViewController: UIViewController, NCShareDetail { + public var downloadLimit: tableDownloadLimit? + public var metadata: tableMetadata! + public var onDismiss: (() -> Void)? + public var share: NCTableShareable! + + @IBOutlet var headerContainerView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + self.setNavigationTitle() + + // Set up header view. + + guard let headerView = (Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader) else { return } + headerContainerView.addSubview(headerView) + headerView.frame = headerContainerView.frame + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.topAnchor.constraint(equalTo: headerContainerView.topAnchor).isActive = true + headerView.bottomAnchor.constraint(equalTo: headerContainerView.bottomAnchor).isActive = true + headerView.leftAnchor.constraint(equalTo: headerContainerView.leftAnchor).isActive = true + headerView.rightAnchor.constraint(equalTo: headerContainerView.rightAnchor).isActive = true + + headerView.setupUI(with: metadata) + + // End editing of inputs when the user taps anywhere else. + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapGesture) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let tableViewController = segue.destination as? NCShareDownloadLimitTableViewController else { + return + } + + tableViewController.initialDownloadLimit = downloadLimit + tableViewController.metadata = metadata + tableViewController.share = share + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } +} diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift index 23d395bcd1..8e5df63e92 100644 --- a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift +++ b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift @@ -71,6 +71,7 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFotterDeleg var share: NCTableShareable! var isNewShare: Bool { share is NCTableShareOptions } var metadata: tableMetadata! + var downloadLimit: tableDownloadLimit? var shareConfig: NCShareConfig! var networking: NCShareNetworking? @@ -165,6 +166,14 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFotterDeleg } switch cellConfig { + case .limitDownload: + let storyboard = UIStoryboard(name: "NCShare", bundle: nil) + guard let viewController = storyboard.instantiateViewController(withIdentifier: "NCShareDownloadLimit") as? NCShareDownloadLimitViewController else { return } + viewController.downloadLimit = self.downloadLimit + viewController.metadata = self.metadata + viewController.share = self.share + viewController.onDismiss = tableView.reloadData + self.navigationController?.pushViewController(viewController, animated: true) case .hideDownload: share.hideDownload.toggle() tableView.reloadData() diff --git a/iOSClient/Share/Advanced/NCShareCells.swift b/iOSClient/Share/Advanced/NCShareCells.swift index 575b57c27d..f7418e8b37 100644 --- a/iOSClient/Share/Advanced/NCShareCells.swift +++ b/iOSClient/Share/Advanced/NCShareCells.swift @@ -192,10 +192,14 @@ enum NCLinkPermission: NCPermission { static let forFile: [NCLinkPermission] = [.allowEdit] } +/// +/// Individual aspects of share. +/// enum NCShareDetails: CaseIterable, NCShareCellConfig { func didSelect(for share: NCTableShareable) { switch self { case .hideDownload: share.hideDownload.toggle() + case .limitDownload: return case .expirationDate: return case .password: return case .note: return @@ -207,6 +211,10 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { switch self { case .hideDownload: return NCShareToggleCell(isOn: share.hideDownload) + case .limitDownload: + let cell = UITableViewCell(style: .value1, reuseIdentifier: "downloadLimit") + cell.accessoryType = .disclosureIndicator + return cell case .expirationDate: return NCShareDateCell(share: share) case .password: return NCShareToggleCell(isOn: !share.password.isEmpty, customIcons: ("lock", "lock_open")) @@ -225,6 +233,7 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { var title: String { switch self { case .hideDownload: return NSLocalizedString("_share_hide_download_", comment: "") + case .limitDownload: return NSLocalizedString("_share_limit_download_", comment: "") case .expirationDate: return NSLocalizedString("_share_expiration_date_", comment: "") case .password: return NSLocalizedString("_share_password_protect_", comment: "") case .note: return NSLocalizedString("_share_note_recipient_", comment: "") @@ -232,7 +241,7 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { } } - case label, hideDownload, expirationDate, password, note + case label, hideDownload, limitDownload, expirationDate, password, note static let forLink: [NCShareDetails] = NCShareDetails.allCases static let forUser: [NCShareDetails] = [.expirationDate, .note] } @@ -248,7 +257,16 @@ struct NCShareConfig { self.resharePermission = parentMetadata.sharePermissionsCollaborationServices let type: NCPermission.Type = share.shareType == NCShareCommon().SHARE_TYPE_LINK ? NCLinkPermission.self : NCUserPermission.self self.permissions = parentMetadata.directory ? (parentMetadata.e2eEncrypted ? type.forDirectoryE2EE(account: parentMetadata.account) : type.forDirectory) : type.forFile - self.advanced = share.shareType == NCShareCommon().SHARE_TYPE_LINK ? NCShareDetails.forLink : NCShareDetails.forUser + + if share.shareType == NCShareCommon().SHARE_TYPE_LINK { + if NCCapabilities.shared.getCapabilities(account: parentMetadata.account).capabilityFileSharingDownloadLimit { + self.advanced = NCShareDetails.forLink + } else { + self.advanced = NCShareDetails.forLink.filter { $0 != .limitDownload } + } + } else { + self.advanced = NCShareDetails.forUser + } } func cellFor(indexPath: IndexPath) -> UITableViewCell? { @@ -275,114 +293,3 @@ struct NCShareConfig { } else { return nil } } } - -class NCShareToggleCell: UITableViewCell { - typealias CustomToggleIcon = (onIconName: String?, offIconName: String?) - init(isOn: Bool, customIcons: CustomToggleIcon? = nil) { - super.init(style: .default, reuseIdentifier: "toggleCell") - self.accessibilityValue = isOn ? NSLocalizedString("_on_", comment: "") : NSLocalizedString("_off_", comment: "") - - guard let customIcons = customIcons, - let iconName = isOn ? customIcons.onIconName : customIcons.offIconName else { - self.accessoryType = isOn ? .checkmark : .none - return - } - let image = NCUtility().loadImage(named: iconName, colors: [NCBrandColor.shared.customer], size: self.frame.height - 26) - self.accessoryView = UIImageView(image: image) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class NCShareDateCell: UITableViewCell { - let picker = UIDatePicker() - let textField = UITextField() - var shareType: Int - var onReload: (() -> Void)? - let shareCommon = NCShareCommon() - - init(share: NCTableShareable) { - self.shareType = share.shareType - super.init(style: .value1, reuseIdentifier: "shareExpDate") - - picker.datePickerMode = .date - picker.minimumDate = Date() - picker.preferredDatePickerStyle = .wheels - picker.action(for: .valueChanged) { datePicker in - guard let datePicker = datePicker as? UIDatePicker else { return } - self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date) - } - accessoryView = textField - - let toolbar = UIToolbar.toolbar { - self.resignFirstResponder() - share.expirationDate = nil - self.onReload?() - } onDone: { - self.resignFirstResponder() - share.expirationDate = self.picker.date as NSDate - self.onReload?() - } - - textField.isAccessibilityElement = false - textField.accessibilityElementsHidden = true - textField.inputAccessoryView = toolbar.wrappedSafeAreaContainer - textField.inputView = picker - - if let expDate = share.expirationDate { - detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date) - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func checkMaximumDate(account: String) { - let defaultExpDays = defaultExpirationDays(account: account) - if defaultExpDays > 0 && isExpireDateEnforced(account: account) { - let enforcedInSecs = TimeInterval(defaultExpDays * 24 * 60 * 60) - self.picker.maximumDate = Date().advanced(by: enforcedInSecs) - } - } - - private func isExpireDateEnforced(account: String) -> Bool { - switch self.shareType { - case shareCommon.SHARE_TYPE_LINK, - shareCommon.SHARE_TYPE_EMAIL, - shareCommon.SHARE_TYPE_GUEST: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateEnforced - case shareCommon.SHARE_TYPE_USER, - shareCommon.SHARE_TYPE_GROUP, - shareCommon.SHARE_TYPE_CIRCLE, - shareCommon.SHARE_TYPE_ROOM: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateEnforced - case shareCommon.SHARE_TYPE_REMOTE, - shareCommon.SHARE_TYPE_REMOTE_GROUP: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateEnforced - default: - return false - } - } - - private func defaultExpirationDays(account: String) -> Int { - switch self.shareType { - case shareCommon.SHARE_TYPE_LINK, - shareCommon.SHARE_TYPE_EMAIL, - shareCommon.SHARE_TYPE_GUEST: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateDays - case shareCommon.SHARE_TYPE_USER, - shareCommon.SHARE_TYPE_GROUP, - shareCommon.SHARE_TYPE_CIRCLE, - shareCommon.SHARE_TYPE_ROOM: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateDays - case shareCommon.SHARE_TYPE_REMOTE, - shareCommon.SHARE_TYPE_REMOTE_GROUP: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateDays - default: - return 0 - } - } -} diff --git a/iOSClient/Share/Advanced/NCShareDateCell.swift b/iOSClient/Share/Advanced/NCShareDateCell.swift new file mode 100644 index 0000000000..fab608f1aa --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareDateCell.swift @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Storch +// SPDX-License-Identifier: GPL-3.0-or-later + +/// +/// Table view cell to manage the expiration date on a share in its details. +/// +class NCShareDateCell: UITableViewCell { + let picker = UIDatePicker() + let textField = UITextField() + var shareType: Int + var onReload: (() -> Void)? + let shareCommon = NCShareCommon() + + init(share: NCTableShareable) { + self.shareType = share.shareType + super.init(style: .value1, reuseIdentifier: "shareExpDate") + + picker.datePickerMode = .date + picker.minimumDate = Date() + picker.preferredDatePickerStyle = .wheels + picker.action(for: .valueChanged) { datePicker in + guard let datePicker = datePicker as? UIDatePicker else { return } + self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date) + } + accessoryView = textField + + let toolbar = UIToolbar.toolbar { + self.resignFirstResponder() + share.expirationDate = nil + self.onReload?() + } onDone: { + self.resignFirstResponder() + share.expirationDate = self.picker.date as NSDate + self.onReload?() + } + + textField.isAccessibilityElement = false + textField.accessibilityElementsHidden = true + textField.inputAccessoryView = toolbar.wrappedSafeAreaContainer + textField.inputView = picker + + if let expDate = share.expirationDate { + detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func checkMaximumDate(account: String) { + let defaultExpDays = defaultExpirationDays(account: account) + if defaultExpDays > 0 && isExpireDateEnforced(account: account) { + let enforcedInSecs = TimeInterval(defaultExpDays * 24 * 60 * 60) + self.picker.maximumDate = Date().advanced(by: enforcedInSecs) + } + } + + private func isExpireDateEnforced(account: String) -> Bool { + switch self.shareType { + case shareCommon.SHARE_TYPE_LINK, + shareCommon.SHARE_TYPE_EMAIL, + shareCommon.SHARE_TYPE_GUEST: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateEnforced + case shareCommon.SHARE_TYPE_USER, + shareCommon.SHARE_TYPE_GROUP, + shareCommon.SHARE_TYPE_CIRCLE, + shareCommon.SHARE_TYPE_ROOM: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateEnforced + case shareCommon.SHARE_TYPE_REMOTE, + shareCommon.SHARE_TYPE_REMOTE_GROUP: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateEnforced + default: + return false + } + } + + private func defaultExpirationDays(account: String) -> Int { + switch self.shareType { + case shareCommon.SHARE_TYPE_LINK, + shareCommon.SHARE_TYPE_EMAIL, + shareCommon.SHARE_TYPE_GUEST: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateDays + case shareCommon.SHARE_TYPE_USER, + shareCommon.SHARE_TYPE_GROUP, + shareCommon.SHARE_TYPE_CIRCLE, + shareCommon.SHARE_TYPE_ROOM: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateDays + case shareCommon.SHARE_TYPE_REMOTE, + shareCommon.SHARE_TYPE_REMOTE_GROUP: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateDays + default: + return 0 + } + } +} diff --git a/iOSClient/Share/Advanced/NCShareToggleCell.swift b/iOSClient/Share/Advanced/NCShareToggleCell.swift new file mode 100644 index 0000000000..b3ff60457a --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareToggleCell.swift @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Storch +// SPDX-License-Identifier: GPL-3.0-or-later + +/// +/// A table view cell for logical switches in the detaills of a share configuration. +/// +class NCShareToggleCell: UITableViewCell { + typealias CustomToggleIcon = (onIconName: String?, offIconName: String?) + init(isOn: Bool, customIcons: CustomToggleIcon? = nil) { + super.init(style: .default, reuseIdentifier: "toggleCell") + self.accessibilityValue = isOn ? NSLocalizedString("_on_", comment: "") : NSLocalizedString("_off_", comment: "") + + guard let customIcons = customIcons, + let iconName = isOn ? customIcons.onIconName : customIcons.offIconName else { + self.accessoryType = isOn ? .checkmark : .none + return + } + let image = NCUtility().loadImage(named: iconName, colors: [NCBrandColor.shared.customer], size: self.frame.height - 26) + self.accessoryView = UIImageView(image: image) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/iOSClient/Share/NCShare.storyboard b/iOSClient/Share/NCShare.storyboard index d06257f31f..e3e3bbd089 100644 --- a/iOSClient/Share/NCShare.storyboard +++ b/iOSClient/Share/NCShare.storyboard @@ -1,9 +1,9 @@ - + - + @@ -247,6 +247,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -282,16 +395,16 @@ - + - + - + diff --git a/iOSClient/Share/NCShareDownloadLimitNetworking.swift b/iOSClient/Share/NCShareDownloadLimitNetworking.swift new file mode 100644 index 0000000000..325e81c5b7 --- /dev/null +++ b/iOSClient/Share/NCShareDownloadLimitNetworking.swift @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +/// +/// Delegate requirements for ``NCShareDownloadLimitNetworking`` to handle results. +/// +protocol NCShareDownloadLimitNetworkingDelegate: AnyObject { + /// + /// The download limit was successfully removed from the share on the server. + /// + func downloadLimitRemoved(by token: String, in account: String) + + /// + /// The download limit was successfully removed from the share on the server. + /// + func downloadLimitSet(to limit: Int, by token: String, in account: String) +} + +/// +/// Share-bound network abstraction for download limits. +/// +class NCShareDownloadLimitNetworking: NSObject { + let account: String + weak var delegate: (any NCShareDownloadLimitNetworkingDelegate)? + weak var view: UIView? + let token: String + + init(account: String, delegate: (any NCShareDownloadLimitNetworkingDelegate)?, token: String) { + self.account = account + self.delegate = delegate + self.token = token + } + + /// + /// Remove the download limit on the share, if existent. + /// + func removeShareDownloadLimit() { + NCActivityIndicator.shared.start(backgroundView: view) + NextcloudKit.shared.removeShareDownloadLimit(account: account, token: token) { error in + NCActivityIndicator.shared.stop() + + if error == .success { + self.delegate?.downloadLimitRemoved(by: self.token, in: self.account) + } else { + NCContentPresenter().showError(error: error) + } + } + } + + /// + /// Set the download limit for the share. + /// + /// - Parameter limit: The new download limit to set. + /// + func setShareDownloadLimit(limit: Int) { + NCActivityIndicator.shared.start(backgroundView: view) + NextcloudKit.shared.setShareDownloadLimit(account: account, token: token, limit: limit) { error in + NCActivityIndicator.shared.stop() + + if error == .success { + self.delegate?.downloadLimitSet(to: limit, by: self.token, in: self.account) + } else { + NCContentPresenter().showError(error: error) + } + } + } +} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index c7aecf1845..5e57335b67 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -677,6 +677,9 @@ "_share_file_drop_" = "File drop (upload only)"; "_share_secure_file_drop_" = "Secure file drop (upload only)"; "_share_hide_download_" = "Hide download"; +"_share_limit_download_" = "Limit downloads"; +"_remaining_share_downloads_" = "%d remaining downloads allowed"; +"_remaining_" = "%d remaining"; "_share_password_protect_" = "Password protect"; "_share_expiration_date_" = "Set expiration date"; "_share_note_recipient_" = "Note to recipient";