NCShareCells.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. //
  2. // NCShareCells.swift
  3. // Nextcloud
  4. //
  5. // Created by Henrik Storch on 18.03.22.
  6. // Copyright © 2022 Henrik Storch. All rights reserved.
  7. //
  8. // Author Henrik Storch <henrik.storch@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import UIKit
  24. protocol NCShareCellConfig {
  25. var title: String { get }
  26. func getCell(for share: NCTableShareable) -> UITableViewCell
  27. func didSelect(for share: NCTableShareable)
  28. }
  29. protocol NCToggleCellConfig: NCShareCellConfig {
  30. func isOn(for share: NCTableShareable) -> Bool
  31. func didChange(_ share: NCTableShareable, to newValue: Bool)
  32. }
  33. extension NCToggleCellConfig {
  34. func getCell(for share: NCTableShareable) -> UITableViewCell {
  35. return NCShareToggleCell(isOn: isOn(for: share))
  36. }
  37. func didSelect(for share: NCTableShareable) {
  38. didChange(share, to: !isOn(for: share))
  39. }
  40. }
  41. protocol NCPermission: NCToggleCellConfig {
  42. static var forDirectory: [Self] { get }
  43. static var forDirectoryE2EE: [Self] { get }
  44. static var forFile: [Self] { get }
  45. func hasResharePermission(for parentPermission: Int) -> Bool
  46. }
  47. enum NCUserPermission: CaseIterable, NCPermission {
  48. func hasResharePermission(for parentPermission: Int) -> Bool {
  49. return ((permissionBitFlag & parentPermission) != 0)
  50. }
  51. var permissionBitFlag: Int {
  52. switch self {
  53. case .reshare: return NCGlobal.shared.permissionShareShare
  54. case .edit: return NCGlobal.shared.permissionUpdateShare
  55. case .create: return NCGlobal.shared.permissionCreateShare
  56. case .delete: return NCGlobal.shared.permissionDeleteShare
  57. }
  58. }
  59. func didChange(_ share: NCTableShareable, to newValue: Bool) {
  60. share.permissions ^= permissionBitFlag
  61. }
  62. func isOn(for share: NCTableShareable) -> Bool {
  63. return (share.permissions & permissionBitFlag) != 0
  64. }
  65. case reshare, edit, create, delete
  66. static let forDirectory: [NCUserPermission] = NCUserPermission.allCases
  67. static let forDirectoryE2EE: [NCUserPermission] = []
  68. static let forFile: [NCUserPermission] = [.reshare, .edit]
  69. var title: String {
  70. switch self {
  71. case .reshare: return NSLocalizedString("_share_can_reshare_", comment: "")
  72. case .edit: return NSLocalizedString("_share_can_change_", comment: "")
  73. case .create: return NSLocalizedString("_share_can_create_", comment: "")
  74. case .delete: return NSLocalizedString("_share_can_delete_", comment: "")
  75. }
  76. }
  77. }
  78. enum NCLinkPermission: NCPermission {
  79. func didChange(_ share: NCTableShareable, to newValue: Bool) {
  80. guard self != .allowEdit || newValue else {
  81. share.permissions = NCGlobal.shared.permissionReadShare
  82. return
  83. }
  84. share.permissions = permissionValue
  85. }
  86. func hasResharePermission(for parentPermission: Int) -> Bool {
  87. permissionValue & parentPermission == permissionValue
  88. }
  89. var permissionValue: Int {
  90. switch self {
  91. case .allowEdit:
  92. return CCUtility.getPermissionsValue(
  93. byCanEdit: true,
  94. andCanCreate: true,
  95. andCanChange: true,
  96. andCanDelete: true,
  97. andCanShare: false,
  98. andIsFolder: false)
  99. case .viewOnly:
  100. return CCUtility.getPermissionsValue(
  101. byCanEdit: false,
  102. andCanCreate: false,
  103. andCanChange: false,
  104. andCanDelete: false,
  105. // not possible to create "read-only" shares without reshare option
  106. // https://github.com/nextcloud/server/blame/f99876997a9119518fe5f7ad3a3a51d33459d4cc/apps/files_sharing/lib/Controller/ShareAPIController.php#L1104-L1107
  107. andCanShare: true,
  108. andIsFolder: true)
  109. case .uploadEdit:
  110. return CCUtility.getPermissionsValue(
  111. byCanEdit: true,
  112. andCanCreate: true,
  113. andCanChange: true,
  114. andCanDelete: true,
  115. andCanShare: false,
  116. andIsFolder: true)
  117. case .fileDrop:
  118. return NCGlobal.shared.permissionCreateShare
  119. case .secureFileDrop:
  120. return NCGlobal.shared.permissionCreateShare
  121. }
  122. }
  123. func isOn(for share: NCTableShareable) -> Bool {
  124. switch self {
  125. case .allowEdit: return CCUtility.isAnyPermission(toEdit: share.permissions)
  126. case .viewOnly: return !CCUtility.isAnyPermission(toEdit: share.permissions) && share.permissions != NCGlobal.shared.permissionCreateShare
  127. case .uploadEdit: return CCUtility.isAnyPermission(toEdit: share.permissions) && share.permissions != NCGlobal.shared.permissionCreateShare
  128. case .fileDrop: return share.permissions == NCGlobal.shared.permissionCreateShare
  129. case .secureFileDrop: return share.permissions == NCGlobal.shared.permissionCreateShare
  130. }
  131. }
  132. var title: String {
  133. switch self {
  134. case .allowEdit: return NSLocalizedString("_share_can_change_", comment: "")
  135. case .viewOnly: return NSLocalizedString("_share_read_only_", comment: "")
  136. case .uploadEdit: return NSLocalizedString("_share_allow_upload_", comment: "")
  137. case .fileDrop: return NSLocalizedString("_share_file_drop_", comment: "")
  138. case .secureFileDrop: return NSLocalizedString("_share_secure_file_drop_", comment: "")
  139. }
  140. }
  141. case allowEdit, viewOnly, uploadEdit, fileDrop, secureFileDrop
  142. static let forDirectory: [NCLinkPermission] = [.viewOnly, .uploadEdit, .fileDrop]
  143. static let forFile: [NCLinkPermission] = [.allowEdit]
  144. static let forDirectoryE2EE: [NCLinkPermission] = [.secureFileDrop]
  145. }
  146. enum NCShareDetails: CaseIterable, NCShareCellConfig {
  147. func didSelect(for share: NCTableShareable) {
  148. switch self {
  149. case .hideDownload: share.hideDownload.toggle()
  150. case .expirationDate: return
  151. case .password: return
  152. case .note: return
  153. case .label: return
  154. }
  155. }
  156. func getCell(for share: NCTableShareable) -> UITableViewCell {
  157. switch self {
  158. case .hideDownload:
  159. return NCShareToggleCell(isOn: share.hideDownload)
  160. case .expirationDate:
  161. return NCShareDateCell(share: share)
  162. case .password: return NCShareToggleCell(isOn: !share.password.isEmpty, customIcons: ("lock", "lock_open"))
  163. case .note:
  164. let cell = UITableViewCell(style: .value1, reuseIdentifier: "shareNote")
  165. cell.detailTextLabel?.text = share.note
  166. cell.accessoryType = .disclosureIndicator
  167. return cell
  168. case .label:
  169. let cell = UITableViewCell(style: .value1, reuseIdentifier: "shareLabel")
  170. cell.detailTextLabel?.text = share.label
  171. return cell
  172. }
  173. }
  174. var title: String {
  175. switch self {
  176. case .hideDownload: return NSLocalizedString("_share_hide_download_", comment: "")
  177. case .expirationDate: return NSLocalizedString("_share_expiration_date_", comment: "")
  178. case .password: return NSLocalizedString("_share_password_protect_", comment: "")
  179. case .note: return NSLocalizedString("_share_note_recipient_", comment: "")
  180. case .label: return NSLocalizedString("_share_link_name_", comment: "")
  181. }
  182. }
  183. case label, hideDownload, expirationDate, password, note
  184. static let forLink: [NCShareDetails] = NCShareDetails.allCases
  185. static let forUser: [NCShareDetails] = [.expirationDate, .note]
  186. }
  187. struct NCShareConfig {
  188. let permissions: [NCPermission]
  189. let advanced: [NCShareDetails]
  190. let share: NCTableShareable
  191. let resharePermission: Int
  192. init(parentMetadata: tableMetadata, share: NCTableShareable) {
  193. self.share = share
  194. self.resharePermission = parentMetadata.sharePermissionsCollaborationServices
  195. let type: NCPermission.Type = share.shareType == NCShareCommon.shared.SHARE_TYPE_LINK ? NCLinkPermission.self : NCUserPermission.self
  196. self.permissions = parentMetadata.directory ? (parentMetadata.e2eEncrypted ? type.forDirectoryE2EE : type.forDirectory) : type.forFile
  197. self.advanced = share.shareType == NCShareCommon.shared.SHARE_TYPE_LINK ? NCShareDetails.forLink : NCShareDetails.forUser
  198. }
  199. func cellFor(indexPath: IndexPath) -> UITableViewCell? {
  200. let cellConfig = config(for: indexPath)
  201. let cell = cellConfig?.getCell(for: share)
  202. cell?.textLabel?.text = cellConfig?.title
  203. if let cellConfig = cellConfig as? NCPermission, !cellConfig.hasResharePermission(for: resharePermission) {
  204. cell?.isUserInteractionEnabled = false
  205. cell?.textLabel?.isEnabled = false
  206. }
  207. return cell
  208. }
  209. func didSelectRow(at indexPath: IndexPath) {
  210. let cellConfig = config(for: indexPath)
  211. cellConfig?.didSelect(for: share)
  212. }
  213. func config(for indexPath: IndexPath) -> NCShareCellConfig? {
  214. if indexPath.section == 0, indexPath.row < permissions.count {
  215. return permissions[indexPath.row]
  216. } else if indexPath.section == 1, indexPath.row < advanced.count {
  217. return advanced[indexPath.row]
  218. } else { return nil }
  219. }
  220. }
  221. class NCShareToggleCell: UITableViewCell {
  222. typealias CustomToggleIcon = (onIconName: String?, offIconName: String?)
  223. init(isOn: Bool, customIcons: CustomToggleIcon? = nil) {
  224. super.init(style: .default, reuseIdentifier: "toggleCell")
  225. self.accessibilityValue = isOn ? NSLocalizedString("_on_", comment: "") : NSLocalizedString("_off_", comment: "")
  226. guard let customIcons = customIcons,
  227. let iconName = isOn ? customIcons.onIconName : customIcons.offIconName else {
  228. self.accessoryType = isOn ? .checkmark : .none
  229. return
  230. }
  231. let image = NCUtility.shared.loadImage(named: iconName, color: NCBrandColor.shared.brandElement, size: self.frame.height - 26)
  232. self.accessoryView = UIImageView(image: image)
  233. }
  234. required init?(coder: NSCoder) {
  235. fatalError("init(coder:) has not been implemented")
  236. }
  237. }
  238. class NCShareDateCell: UITableViewCell {
  239. let picker = UIDatePicker()
  240. let textField = UITextField()
  241. var shareType: Int
  242. var onReload: (() -> Void)?
  243. init(share: NCTableShareable) {
  244. self.shareType = share.shareType
  245. super.init(style: .value1, reuseIdentifier: "shareExpDate")
  246. picker.datePickerMode = .date
  247. picker.minimumDate = Date()
  248. picker.preferredDatePickerStyle = .wheels
  249. picker.action(for: .valueChanged) { datePicker in
  250. guard let datePicker = datePicker as? UIDatePicker else { return }
  251. self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date)
  252. }
  253. accessoryView = textField
  254. let toolbar = UIToolbar.toolbar {
  255. self.resignFirstResponder()
  256. share.expirationDate = nil
  257. self.onReload?()
  258. } completion: {
  259. self.resignFirstResponder()
  260. share.expirationDate = self.picker.date as NSDate
  261. self.onReload?()
  262. }
  263. textField.isAccessibilityElement = false
  264. textField.accessibilityElementsHidden = true
  265. textField.inputAccessoryView = toolbar.wrappedSafeAreaContainer
  266. textField.inputView = picker
  267. if let expDate = share.expirationDate {
  268. detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date)
  269. }
  270. }
  271. required public init?(coder: NSCoder) {
  272. fatalError("init(coder:) has not been implemented")
  273. }
  274. func checkMaximumDate(account: String) {
  275. let defaultExpDays = defaultExpirationDays(account: account)
  276. if defaultExpDays > 0 && isExpireDateEnforced(account: account) {
  277. let enforcedInSecs = TimeInterval(defaultExpDays * 24 * 60 * 60)
  278. self.picker.maximumDate = Date().advanced(by: enforcedInSecs)
  279. }
  280. }
  281. private func isExpireDateEnforced(account: String) -> Bool {
  282. switch self.shareType {
  283. case NCShareCommon.shared.SHARE_TYPE_LINK,
  284. NCShareCommon.shared.SHARE_TYPE_EMAIL,
  285. NCShareCommon.shared.SHARE_TYPE_GUEST:
  286. return NCGlobal.shared.capabilityFileSharingPubExpireDateEnforced
  287. case NCShareCommon.shared.SHARE_TYPE_USER,
  288. NCShareCommon.shared.SHARE_TYPE_GROUP,
  289. NCShareCommon.shared.SHARE_TYPE_CIRCLE,
  290. NCShareCommon.shared.SHARE_TYPE_ROOM:
  291. return NCGlobal.shared.capabilityFileSharingInternalExpireDateEnforced
  292. case NCShareCommon.shared.SHARE_TYPE_REMOTE,
  293. NCShareCommon.shared.SHARE_TYPE_REMOTE_GROUP:
  294. return NCGlobal.shared.capabilityFileSharingRemoteExpireDateEnforced
  295. default:
  296. return false
  297. }
  298. }
  299. private func defaultExpirationDays(account: String) -> Int {
  300. switch self.shareType {
  301. case NCShareCommon.shared.SHARE_TYPE_LINK,
  302. NCShareCommon.shared.SHARE_TYPE_EMAIL,
  303. NCShareCommon.shared.SHARE_TYPE_GUEST:
  304. return NCGlobal.shared.capabilityFileSharingPubExpireDateDays
  305. case NCShareCommon.shared.SHARE_TYPE_USER,
  306. NCShareCommon.shared.SHARE_TYPE_GROUP,
  307. NCShareCommon.shared.SHARE_TYPE_CIRCLE,
  308. NCShareCommon.shared.SHARE_TYPE_ROOM:
  309. return NCGlobal.shared.capabilityFileSharingInternalExpireDateDays
  310. case NCShareCommon.shared.SHARE_TYPE_REMOTE,
  311. NCShareCommon.shared.SHARE_TYPE_REMOTE_GROUP:
  312. return NCGlobal.shared.capabilityFileSharingRemoteExpireDateDays
  313. default:
  314. return 0
  315. }
  316. }
  317. }