diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index e9cfa0612..d4a051e02 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -433,6 +433,12 @@ 74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; }; 74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; }; B330CB452CB5735300C21E03 /* UnauthorizedErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */; }; + B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */; }; + B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */; }; + B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */; }; + B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C532B2D142BE000F30FE9 /* URLValidator.swift */; }; + B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */; }; + B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */; }; B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; }; /* End PBXBuildFile section */ @@ -1040,6 +1046,13 @@ 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = ""; }; + B34C53212D1355D900F30FE9 /* cloud-access-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "cloud-access-swift"; path = "../cloud-access-swift"; sourceTree = SOURCE_ROOT; }; + B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = ""; }; + B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = ""; }; + B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointURLSetting.swift; sourceTree = ""; }; + B34C532B2D142BE000F30FE9 /* URLValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidator.swift; sourceTree = ""; }; + B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewController.swift; sourceTree = ""; }; + B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewModel.swift; sourceTree = ""; }; B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1378,6 +1391,12 @@ 4A644B45267A3D21008CBB9A /* CreateNewVault */ = { isa = PBXGroup; children = ( + B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */, + B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */, + B34C532B2D142BE000F30FE9 /* URLValidator.swift */, + B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */, + B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */, + B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */, 4A644B4A267B4C08008CBB9A /* CreateNewVaultChooseFolderViewController.swift */, 4A53CC16267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift */, 4A644B4C267B55E4008CBB9A /* CreateNewVaultCoordinator.swift */, @@ -2482,7 +2501,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; + shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; }; 742595D72552EE0000A8A008 /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; @@ -2718,6 +2737,7 @@ 4A6A521D268B7C8F006F7368 /* BaseNavigationController.swift in Sources */, 4AC005F127C3D80B006FFE87 /* PremiumManager.swift in Sources */, 4ADD2342267383BE00374E4E /* AddVaultSuccessViewModel.swift in Sources */, + B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */, 4AB1D4F827D68026009060AB /* IAPHeaderView.swift in Sources */, 4A79E26926B16993008C9959 /* ActionButton.swift in Sources */, 4AF91CD925A722A600ACF01E /* VaultInfo.swift in Sources */, @@ -2775,6 +2795,7 @@ 4A4B7E7426B954D2009BFDB1 /* HeaderFooterViewModel.swift in Sources */, 4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */, 74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */, + B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */, 4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */, 4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */, 4AF4535F272066A600CF1919 /* RenameVaultViewController.swift in Sources */, @@ -2790,6 +2811,7 @@ 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */, 4A8D060525C82F1F0082C5F7 /* AddVaultSuccesing.swift in Sources */, 4A61F6B9274582E3007AA422 /* StaticUITableViewController.swift in Sources */, + B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */, 4A21B49426BC0127000D13DF /* BindableAttributedTextHeaderFooterViewModel.swift in Sources */, 740D367E266A18DF0058744D /* SettingsViewController.swift in Sources */, 4AF45356271F2A8300CF1919 /* RenameVaultViewModel.swift in Sources */, @@ -2820,17 +2842,20 @@ 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */, 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */, 4AC86270273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift in Sources */, + B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */, 4AB1D4FD27D69BB2009060AB /* TrialCell.swift in Sources */, 4A2FD08225B5E2BA008565C8 /* VaultInstalling.swift in Sources */, 74C2BC5226E8FCD000BCAA03 /* PurchaseCoordinator.swift in Sources */, 4A53B6D32722F92D000DC367 /* MoveVaultViewModel.swift in Sources */, 4A4B7E4426B2B1A5009BFDB1 /* BindableTableViewCellViewModel.swift in Sources */, + B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */, 4AED9A69286B303000352951 /* S3Authenticator+VC.swift in Sources */, 4ADBD35827284BAB00B19B5C /* MoveVaultViewController.swift in Sources */, 7408E6CD26779BCC00D7FAEA /* AboutViewModel.swift in Sources */, 4A8A6424286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift in Sources */, 4AB1D4FF27D69C9A009060AB /* DisclosureCell.swift in Sources */, 4A7077FF278DC2ED00AEF4CE /* VaultKeepUnlockedViewController.swift in Sources */, + B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */, 4A21B49C26BD68C2000D13DF /* UIControl+Publisher.swift in Sources */, 4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */, 4A4B7E4A26B2C071009BFDB1 /* ButtonCellViewModel.swift in Sources */, diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index 3f34fc168..d1dbefa05 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -11,12 +11,13 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import UIKit -class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { +class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator, SharePointURLSetting { var navigationController: UINavigationController var childCoordinators = [Coordinator]() weak var parentCoordinator: Coordinator? private let vaultName: String + private var currentSharePointAccount: AccountInfo? init(navigationController: UINavigationController, vaultName: String) { self.navigationController = navigationController @@ -24,7 +25,7 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA } func start() { - let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header")) + let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header")) let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("addVault.createNewVault.title") chooseCloudVC.coordinator = self @@ -44,15 +45,59 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared) - authenticator.authenticate(cloudProviderType, from: viewController).then { account in - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - self.startFolderChooser(with: provider, account: account) + authenticator.authenticate(cloudProviderType, from: viewController).then { _ in } } + func showEnterSharePointURL(for account: AccountInfo) { + let viewModel = EnterSharePointURLViewModel(account: account) + let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel) + enterURLVC.coordinator = self + navigationController.pushViewController(enterURLVC, animated: true) + } + func selectedAccont(_ account: AccountInfo) throws { - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - startFolderChooser(with: provider, account: account.cloudProviderAccount) + if account.cloudProviderType == .sharePoint { + currentSharePointAccount = account + showEnterSharePointURL(for: account) + } else { + let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } + } + + func setSharePointURL(_ url: String) { + guard let account = currentSharePointAccount else { return } + + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let discovery = MicrosoftGraphDiscovery(credential: credential) + + showDriveList(discovery: discovery, sharePointURL: url) + } + + private func showDriveList(discovery: MicrosoftGraphDiscovery, sharePointURL: String) { + guard let account = currentSharePointAccount else { return } + let viewModel = SharePointDriveListViewModel(discovery: discovery, sharePointURL: sharePointURL, account: account) + viewModel.didSelectDrive = { [weak self] drive in + self?.handleDriveSelection(drive: drive) + } + let driveListVC = SharePointDriveListViewController(viewModel: viewModel) + navigationController.pushViewController(driveListVC, animated: true) + } + + private func handleDriveSelection(drive: MicrosoftGraphDrive) { + guard let account = currentSharePointAccount else { + print("No current SharePoint account available") + return + } + do { + try MicrosoftGraphDriveManager.shared.saveDriveToKeychain(drive, for: account.accountUID) + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } catch { + handleError(error, for: navigationController) + } } private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) { diff --git a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift new file mode 100644 index 000000000..c7694ed2c --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift @@ -0,0 +1,44 @@ +// +//  EnterSharePointURLViewController.swift +//  Cryptomator +// +//  Created by Majid Achhoud on 03.12.24. +// + +import Combine +import CryptomatorCommonCore +import UIKit + +class EnterSharePointURLViewController: SingleSectionStaticUITableViewController { + weak var coordinator: (SharePointURLSetting & Coordinator)? + private var viewModel: EnterSharePointURLViewModelProtocol + private var lastReturnButtonPressedSubscriber: AnyCancellable? + init(viewModel: EnterSharePointURLViewModelProtocol) { + self.viewModel = viewModel + super.init(viewModel: viewModel) + } + + override func viewDidLoad() { + super.viewDidLoad() + let doneButton = UIBarButtonItem(title: LocalizedString.getValue("common.button.next"), style: .done, target: self, action: #selector(nextButtonClicked)) + navigationItem.rightBarButtonItem = doneButton + lastReturnButtonPressedSubscriber = viewModel.lastReturnButtonPressed.sink { [weak self] in + self?.lastReturnButtonPressedAction() + } + } + + @objc func nextButtonClicked() { + guard let coordinator = coordinator else { return } + do { + let url = try viewModel.getValidatedSharePointURL() + coordinator.setSharePointURL(url) + } catch { + print("Error validating SharePoint URL: \(error)") + coordinator.handleError(error, for: self) + } + } + + func lastReturnButtonPressedAction() { + nextButtonClicked() + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift new file mode 100644 index 000000000..943668420 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift @@ -0,0 +1,72 @@ +// +// EnterSharePointURLViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Combine +import CryptomatorCommonCore +import Foundation + +protocol EnterSharePointURLViewModelProtocol: SingleSectionTableViewModel, ReturnButtonSupport { + func getValidatedSharePointURL() throws -> String +} + +class EnterSharePointURLViewModel: SingleSectionTableViewModel, EnterSharePointURLViewModelProtocol { + let account: AccountInfo + init(account: AccountInfo) { + self.account = account + } + + var lastReturnButtonPressed: AnyPublisher { + return setupReturnButtonSupport(for: [sharePointURLCellViewModel], subscribers: &subscribers) + } + + override var cells: [TableViewCellViewModel] { + return [sharePointURLCellViewModel] + } + + override var title: String? { + return LocalizedString.getValue("addVault.enterSharePointURL.title") + } + + let sharePointURLCellViewModel = TextFieldCellViewModel( + type: .normal, + placeholder: LocalizedString.getValue("addVault.enterSharePointURL.placeholder"), + isInitialFirstResponder: true + ) + var trimmedSharePointURL: String { + return sharePointURLCellViewModel.input.value.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private lazy var subscribers = Set() + func getValidatedSharePointURL() throws -> String { + guard !trimmedSharePointURL.isEmpty else { + throw EnterSharePointURLViewModelError.emptyURL + } + try URLValidator.validateSharePointURL(urlString: trimmedSharePointURL) + return trimmedSharePointURL + } + + override func getHeaderTitle(for section: Int) -> String? { + guard section == 0 else { + return nil + } + return LocalizedString.getValue("addVault.enterSharePointURL.header.title") + } +} + +enum EnterSharePointURLViewModelError: LocalizedError { + case emptyURL + case invalidURL + var errorDescription: String? { + switch self { + case .emptyURL: + return LocalizedString.getValue("addVault.enterSharePointURL.error.emptyURL") + case .invalidURL: + return LocalizedString.getValue("addVault.enterSharePointURL.error.invalidURL") + } + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift new file mode 100644 index 000000000..d1aecb92c --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift @@ -0,0 +1,69 @@ +// +// SharePointDriveListViewController.swift +// Cryptomator +// +// Created by Majid Achhoud +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import UIKit + +class SharePointDriveListViewController: BaseUITableViewController { + private var viewModel: SharePointDriveListViewModel + + init(viewModel: SharePointDriveListViewModel) { + self.viewModel = viewModel + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(CloudCell.self, forCellReuseIdentifier: "SharePointDriveCell") + viewModel.reloadData = { [weak self] in + self?.tableView.reloadData() + } + + self.title = LocalizedString.getValue("addVault.selectDrive.navigation.title") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.drives.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else { + fatalError("Could not dequeue CloudCell") + } + + let drive = viewModel.drives[indexPath.row] + configure(cell, with: drive) + + return cell + } + + // MARK: - Styling Configuration + + private func configure(_ cell: CloudCell, with drive: MicrosoftGraphDrive) { + cell.textLabel?.text = drive.name + cell.imageView?.image = UIImage(systemName: "folder") + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedDrive = viewModel.drives[indexPath.row] + viewModel.selectDrive(selectedDrive) + tableView.deselectRow(at: indexPath, animated: true) + } +} + diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift new file mode 100644 index 000000000..4cc369e49 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift @@ -0,0 +1,75 @@ +// +// SharePointDriveListViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +class SharePointDriveListViewModel: SingleSectionTableViewModel { + private let discovery: MicrosoftGraphDiscovery + private let sharePointURL: String + private let account: AccountInfo + + var drives: [MicrosoftGraphDrive] = [] { + didSet { + reloadData?() + } + } + + var reloadData: (() -> Void)? + var didSelectDrive: ((MicrosoftGraphDrive) -> Void)? + + init(discovery: MicrosoftGraphDiscovery, sharePointURL: String, account: AccountInfo) { + self.discovery = discovery + self.sharePointURL = sharePointURL + self.account = account + super.init() + fetchSiteAndDrives() + } + + func selectDrive(_ drive: MicrosoftGraphDrive) { + didSelectDrive?(drive) + } + + private func fetchSiteAndDrives() { + guard let urlComponents = URL(string: sharePointURL), + let hostName = urlComponents.host else { + print("Invalid SharePoint URL") + return + } + + var serverRelativePath = urlComponents.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + if serverRelativePath.hasPrefix("sites/") { + serverRelativePath = String(serverRelativePath.dropFirst("sites/".count)) + } + + discovery.fetchSharePointSite(for: hostName, serverRelativePath: serverRelativePath) + .then { site in + self.fetchDrives(for: site.identifier) + }.catch { error in + print("Failed to fetch SharePoint site: \(error)") + } + } + + private func fetchDrives(for siteIdentifier: String) { + discovery.fetchSharePointDocumentLibraries(for: siteIdentifier).then { drives in + self.drives = drives + }.catch { error in + print("Failed to fetch drives: \(error)") + } + } + + override func getHeaderTitle(for section: Int) -> String? { + guard section == 0 else { return nil } + return LocalizedString.getValue("addVault.selectDrive.navigation.title") + } + + override var title: String? { + return LocalizedString.getValue("addVault.selectDrive.header.description") + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift b/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift new file mode 100644 index 000000000..2ee2e5a90 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift @@ -0,0 +1,14 @@ +// +// SharePointURLSetting.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import UIKit + +protocol SharePointURLSetting: AnyObject { + func setSharePointURL(_ url: String) +} diff --git a/Cryptomator/AddVault/CreateNewVault/URLValidator.swift b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift new file mode 100644 index 000000000..87ed27bc0 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift @@ -0,0 +1,42 @@ +// +// URLValidator.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import Foundation + +public enum URLValidatorError: Error, Equatable { + case invalidURLFormat +} + +extension URLValidatorError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidURLFormat: + return LocalizedString.getValue("addVault.enterSharePointURL.error.invalidURL") + } + } +} + +public enum URLValidator { + public static func validateSharePointURL(urlString: String) throws { + guard let url = URL(string: urlString) else { + throw URLValidatorError.invalidURLFormat + } + + guard url.scheme == "https", + let host = url.host, + host.contains(".sharepoint.com") else { + throw URLValidatorError.invalidURLFormat + } + + let path = url.path + guard path.contains("/sites/") else { + throw URLValidatorError.invalidURLFormat + } + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index cb5a01c17..7a0a41c74 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -15,17 +15,19 @@ import Foundation import Promises import UIKit -class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { +class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator, SharePointURLSetting { var navigationController: UINavigationController var childCoordinators = [Coordinator]() weak var parentCoordinator: AddVaultCoordinator? + private var currentSharePointAccount: AccountInfo? + init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { - let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.openExistingVault.chooseCloud.header")) + let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.openExistingVault.chooseCloud.header")) let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("addVault.openExistingVault.title") chooseCloudVC.coordinator = self @@ -43,6 +45,13 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } } + func showEnterSharePointURL(for account: AccountInfo) { + let viewModel = EnterSharePointURLViewModel(account: account) + let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel) + enterURLVC.coordinator = self + navigationController.pushViewController(enterURLVC, animated: true) + } + func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared) authenticator.authenticate(cloudProviderType, from: viewController).then { account in @@ -52,8 +61,47 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } func selectedAccont(_ account: AccountInfo) throws { - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - startFolderChooser(with: provider, account: account.cloudProviderAccount) + if account.cloudProviderType == .sharePoint { + currentSharePointAccount = account + showEnterSharePointURL(for: account) + } else { + let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } + } + + func setSharePointURL(_ url: String) { + guard let account = currentSharePointAccount else { return } + + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let discovery = MicrosoftGraphDiscovery(credential: credential) + + showDriveList(discovery: discovery, sharePointURL: url) + } + + private func showDriveList(discovery: MicrosoftGraphDiscovery, sharePointURL: String) { + guard let account = currentSharePointAccount else { return } + let viewModel = SharePointDriveListViewModel(discovery: discovery, sharePointURL: sharePointURL, account: account) + viewModel.didSelectDrive = { [weak self] drive in + self?.handleDriveSelection(drive: drive) + } + let driveListVC = SharePointDriveListViewController(viewModel: viewModel) + navigationController.pushViewController(driveListVC, animated: true) + } + + private func handleDriveSelection(drive: MicrosoftGraphDrive) { + guard let account = currentSharePointAccount else { + print("No current SharePoint account available") + return + } + do { + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } catch { + print("Error creating provider: \(error)") + handleError(error, for: navigationController) + } } private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) { diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 578578501..f9a96a4b7 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -41,10 +41,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: nil) do { - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: nil) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: nil) } catch { DDLogError("Setting up OneDrive failed with error: \(error)") } @@ -87,7 +87,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } else if url.scheme == CloudAccessSecrets.googleDriveRedirectURLScheme { return GoogleDriveAuthenticator.currentAuthorizationFlow?.resumeExternalUserAgentFlow(with: url) ?? false - } else if url.scheme == CloudAccessSecrets.oneDriveRedirectURIScheme { + } else if url.scheme == CloudAccessSecrets.microsoftGraphRedirectURIScheme { return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) } return false diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index f32d81180..31700c3e5 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift @@ -142,7 +142,7 @@ class AccountListViewController: ListViewController, ASWebAu private func supportsEditing(_ cloudProviderType: CloudProviderType) -> Bool { switch cloudProviderType { - case .box, .dropbox, .googleDrive, .localFileSystem, .oneDrive, .pCloud: + case .box, .dropbox, .googleDrive, .localFileSystem, .oneDrive, .sharePoint, .pCloud: return false case .s3, .webDAV: return true diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift index 79d62b8ff..ef2dd81f3 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift @@ -75,6 +75,7 @@ class AccountListViewModel: AccountListViewModelProtocol { } } + // swiftlint:disable:next cyclomatic_complexity func createAccountCellContent(from accountInfo: AccountInfo) throws -> AccountCellContent { switch cloudProviderType { case .box: @@ -87,7 +88,7 @@ class AccountListViewModel: AccountListViewModelProtocol { case .localFileSystem: throw AccountListError.unsupportedCloudProviderType case .oneDrive: - let credential = try OneDriveCredential(with: accountInfo.accountUID) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountInfo.accountUID) return try createAccountCellContent(for: credential) case .pCloud: return createAccountCellContentPlaceholder() @@ -97,6 +98,9 @@ class AccountListViewModel: AccountListViewModelProtocol { } let displayName = try S3CredentialManager.shared.getDisplayName(for: credential) return createAccountCellContent(for: credential, displayName: displayName) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: accountInfo.accountUID) + return try createAccountCellContent(for: credential) case .webDAV: guard let credential = WebDAVCredentialManager.shared.getCredentialFromKeychain(with: accountInfo.accountUID) else { throw CloudProviderAccountError.accountNotFoundError @@ -120,7 +124,7 @@ class AccountListViewModel: AccountListViewModelProtocol { return AccountCellContent(mainLabelText: username, detailLabelText: nil) } - func createAccountCellContent(for credential: OneDriveCredential) throws -> AccountCellContent { + func createAccountCellContent(for credential: MicrosoftGraphCredential) throws -> AccountCellContent { let username = try credential.getUsername() return AccountCellContent(mainLabelText: username, detailLabelText: nil) } diff --git a/Cryptomator/Common/CloudAuthenticator.swift b/Cryptomator/Common/CloudAuthenticator.swift index 21258e0ef..982486272 100644 --- a/Cryptomator/Common/CloudAuthenticator.swift +++ b/Cryptomator/Common/CloudAuthenticator.swift @@ -44,13 +44,21 @@ class CloudAuthenticator { } func authenticateOneDrive(from viewController: UIViewController) -> Promise { - OneDriveAuthenticator.authenticate(from: viewController).then { credential -> CloudProviderAccount in + return MicrosoftGraphAuthenticator.authenticateForOneDrive(from: viewController).then { credential -> CloudProviderAccount in let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .oneDrive) try self.accountManager.saveNewAccount(account) return account } } + func authenticateSharePoint(from viewController: UIViewController) -> Promise { + return MicrosoftGraphAuthenticator.authenticateForSharePoint(from: viewController).then { credential -> CloudProviderAccount in + let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .sharePoint) + try self.accountManager.saveNewAccount(account) + return account + } + } + func authenticatePCloud(from viewController: UIViewController) -> Promise { return PCloudAuthenticator.authenticate(from: viewController).then { credential -> CloudProviderAccount in try credential.saveToKeychain() @@ -103,6 +111,8 @@ class CloudAuthenticator { return authenticateOneDrive(from: viewController) case .pCloud: return authenticatePCloud(from: viewController) + case .sharePoint: + return authenticateSharePoint(from: viewController) case .s3: return authenticateS3(from: viewController) case .webDAV: @@ -110,6 +120,7 @@ class CloudAuthenticator { } } + // swiftlint:disable:next cyclomatic_complexity func deauthenticate(account: CloudProviderAccount) throws { switch account.cloudProviderType { case .box: @@ -125,13 +136,16 @@ class CloudAuthenticator { case .localFileSystem: break case .oneDrive: - let credential = try OneDriveCredential(with: account.accountUID) + let credential = MicrosoftGraphCredential.createForOneDrive(with: account.accountUID) try credential.deauthenticate() case .pCloud: let credential = try PCloudCredential(userID: account.accountUID) try credential.deauthenticate() case .s3: try S3CredentialManager.shared.removeCredential(with: account.accountUID) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + try credential.deauthenticate() case .webDAV: try WebDAVCredentialManager.shared.removeCredentialFromKeychain(with: account.accountUID) } diff --git a/Cryptomator/Common/CloudProviderType+Localization.swift b/Cryptomator/Common/CloudProviderType+Localization.swift index 79b2d8ba0..92aad6a3d 100644 --- a/Cryptomator/Common/CloudProviderType+Localization.swift +++ b/Cryptomator/Common/CloudProviderType+Localization.swift @@ -22,6 +22,8 @@ extension CloudProviderType { return localFileSystemType.localizedString() case .oneDrive: return "OneDrive" + case .sharePoint: + return "SharePoint" case .pCloud: return "pCloud" case .s3: diff --git a/Cryptomator/Common/UIImage+CloudProviderType.swift b/Cryptomator/Common/UIImage+CloudProviderType.swift index 4e2a4b09c..d8bb007cf 100644 --- a/Cryptomator/Common/UIImage+CloudProviderType.swift +++ b/Cryptomator/Common/UIImage+CloudProviderType.swift @@ -28,6 +28,8 @@ extension UIImage { assetName = UIImage.getVaultIcon(for: localFileSystemType) case .oneDrive: assetName = "onedrive-vault" + case .sharePoint: + assetName = "sharepoint-vault" case .pCloud: assetName = "pcloud-vault" case .s3: @@ -63,6 +65,8 @@ extension UIImage { assetName = UIImage.getStorageIcon(for: localFileSystemType) case .oneDrive: assetName = "onedrive" + case .sharePoint: + assetName = "sharepoint" case .pCloud: assetName = "pcloud" case .s3: diff --git a/Cryptomator/Settings/SettingsCoordinator.swift b/Cryptomator/Settings/SettingsCoordinator.swift index a2a3c5ba6..32e8705b3 100644 --- a/Cryptomator/Settings/SettingsCoordinator.swift +++ b/Cryptomator/Settings/SettingsCoordinator.swift @@ -49,7 +49,7 @@ class SettingsCoordinator: Coordinator { } func showCloudServices() { - let viewModel = ChooseCloudViewModel(clouds: [.dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "") + let viewModel = ChooseCloudViewModel(clouds: [.dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "") let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("settings.cloudServices") chooseCloudVC.coordinator = self diff --git a/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift b/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift index db95fee78..dda6c156d 100644 --- a/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift +++ b/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import CryptomatorCloudAccessCore import Foundation class UnlockSectionFooterViewModel: HeaderFooterViewModel { @@ -31,21 +32,23 @@ class UnlockSectionFooterViewModel: HeaderFooterViewModel { } var biometryTypeName: String? + var vaultInfo: VaultInfo - init(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration) { + init(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration, vaultInfo: VaultInfo) { self.vaultUnlocked = vaultUnlocked self.biometricalUnlockEnabled = biometricalUnlockEnabled self.biometryTypeName = biometryTypeName - let titleText = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration) + let titleText = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration, vaultInfo: vaultInfo) self.title = Bindable(titleText) self.keepUnlockedDuration = keepUnlockedDuration + self.vaultInfo = vaultInfo } private func updateTitle() { - title.value = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration) + title.value = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration, vaultInfo: vaultInfo) } - private static func getTitleText(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration) -> String { + private static func getTitleText(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration, vaultInfo: VaultInfo) -> String { let unlockedText: String if vaultUnlocked { unlockedText = LocalizedString.getValue("vaultDetail.unlocked.footer") @@ -62,7 +65,7 @@ class UnlockSectionFooterViewModel: HeaderFooterViewModel { keepUnlockedText = String(format: LocalizedString.getValue("vaultDetail.keepUnlocked.footer.limitedDuration"), keepUnlockedDuration.description ?? "") } var footerText = "\(unlockedText)\n\n\(keepUnlockedText)" - if let biometryTypeName = biometryTypeName { + if vaultInfo.vaultConfigType != .hub, let biometryTypeName = biometryTypeName { let biometricalUnlockText: String if biometricalUnlockEnabled { biometricalUnlockText = String(format: LocalizedString.getValue("vaultDetail.enabledBiometricalUnlock.footer"), biometryTypeName) diff --git a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift index b2b8b2fe1..b778ae133 100644 --- a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift @@ -42,6 +42,7 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode return String(format: LocalizedString.getValue("vaultDetail.info.footer.accountInfo"), username, vault.cloudProviderType.localizedString()) + " " } + // swiftlint:disable:next cyclomatic_complexity func getUsername() -> String? { switch vault.cloudProviderType { case .box: @@ -59,8 +60,8 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode case .localFileSystem: return nil case .oneDrive: - let credential = try? OneDriveCredential(with: vault.delegateAccountUID) - return try? credential?.getUsername() + let credential = MicrosoftGraphCredential.createForOneDrive(with: vault.delegateAccountUID) + return try? credential.getUsername() case .pCloud: guard let credential = try? PCloudCredential(userID: vault.delegateAccountUID) else { return nil @@ -72,6 +73,9 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode return nil } return displayName + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: vault.delegateAccountUID) + return try? credential.getUsername() case .webDAV: let credential = WebDAVCredentialManager.shared.getCredentialFromKeychain(with: vault.delegateAccountUID) return credential?.username diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 0bc2872ae..077550f94 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -121,7 +121,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private var lockSectionCells: [BindableTableViewCellViewModel] { var cells: [BindableTableViewCellViewModel] = [lockButton, keepUnlockedCellViewModel] - if let biometryTypeName = context.enrolledBiometricsAuthenticationName() { + if vaultInfo.vaultConfigType != .hub, let biometryTypeName = context.enrolledBiometricsAuthenticationName() { let switchCellViewModel = getSwitchCellViewModel(biometryTypeName: biometryTypeName) cells.append(switchCellViewModel) } @@ -146,7 +146,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { .lockingSection: unlockSectionFooterViewModel, .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] - private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value) + private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value, vaultInfo: vaultInfo) private lazy var vaultInfoCellViewModel = BindableTableViewCellViewModel(title: vaultInfo.vaultName, detailTitle: vaultInfo.vaultPath.path, detailTitleTextColor: .secondaryLabel, image: UIImage(vaultIconFor: vaultInfo.cloudProviderType, state: .normal), selectionStyle: .none) private lazy var renameVaultCellViewModel = ButtonCellViewModel.createDisclosureButton(action: VaultDetailButtonAction.showRenameVault, title: LocalizedString.getValue("vaultDetail.button.renameVault"), detailTitle: vaultName) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift index 20ce5215b..d83818dc7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift @@ -25,6 +25,7 @@ class CryptomatorKeychain: CryptomatorKeychainType { static let bundleId = CryptomatorConstants.mainAppBundleId static let box = CryptomatorKeychain(service: "box.auth") static let pCloud = CryptomatorKeychain(service: "pCloud.auth") + static let microsoftGraph = CryptomatorKeychain(service: "microsoftGraph.auth") static let s3 = CryptomatorKeychain(service: "s3.auth") static let webDAV = CryptomatorKeychain(service: "webDAV.auth") static let localFileSystem = CryptomatorKeychain(service: "localFileSystem.auth") diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 7347d8843..78841b8d8 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -20,6 +20,7 @@ public enum HubAuthenticationFlow { case needsDeviceRegistration case licenseExceeded case requiresAccountInitialization(at: URL) + case vaultArchived } public struct HubAuthenticationFlowSuccess { @@ -79,7 +80,9 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .requiresAccountInitialization(at: profileURL) case .legacyHubVersion: throw CryptomatorHubAuthenticatorError.incompatibleHubVersion - } + case .vaultArchived: + return .vaultArchived + } let retrieveUserPrivateKeyResponse = try await getUserKey(apiBaseURL: apiBaseURL, authState: authState) @@ -240,8 +243,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(encryptedVaultKey: body, header: httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded - case 403, 410: + case 403: return .accessNotGranted + case 410: + return .vaultArchived case 404: return .legacyHubVersion case 449: @@ -305,6 +310,8 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving case requiresAccountInitialization(at: URL) // 404 case legacyHubVersion + // 410 + case vaultArchived } private struct DeviceDto: Codable { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index 77e3c2815..4c6aecae0 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -20,7 +20,9 @@ public struct HubAuthenticationView: View { onRegisterTap: { Task { await viewModel.register() }} ) case .accessNotGranted: - HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) + CryptomatorErrorWithRefreshView(headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted"), onRefresh: { Task { await viewModel.refresh() }}) + case .vaultArchived: + CryptomatorErrorWithRefreshView(headerTitle: LocalizedString.getValue("hubAuthentication.vaultArchived"), onRefresh: { Task { await viewModel.refresh() }}) case .licenseExceeded: CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded")) case let .error(description): diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index ee81e7354..c1b31c90d 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -32,6 +32,7 @@ public final class HubAuthenticationViewModel: ObservableObject { case licenseExceeded case deviceRegistration(DeviceRegistration) case error(description: String) + case vaultArchived } public enum DeviceRegistration: Equatable { @@ -108,7 +109,9 @@ public final class HubAuthenticationViewModel: ObservableObject { await setState(to: .licenseExceeded) case let .requiresAccountInitialization(profileURL): await delegate?.hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: profileURL) - } + case .vaultArchived: + await setState(to: .vaultArchived) + } } private func receivedExistingKey(_ flowResponse: HubAuthenticationFlowSuccess) async { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift deleted file mode 100644 index c0328584f..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct HubAccessNotGrantedView: View { - var onRefresh: () -> Void - - var body: some View { - CryptomatorSimpleButtonView( - buttonTitle: LocalizedString.getValue("common.button.refresh"), - onButtonTap: onRefresh, - headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted") - ) - } -} - -struct HubDeviceRegisteredSuccessfullyView_Previews: PreviewProvider { - static var previews: some View { - HubAccessNotGrantedView(onRefresh: {}) - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift new file mode 100644 index 000000000..776bee568 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct CryptomatorErrorWithRefreshView: View { + var headerTitle: String + var onRefresh: () -> Void + + var body: some View { + CryptomatorSimpleButtonView( + buttonTitle: LocalizedString.getValue("common.button.refresh"), + onButtonTap: onRefresh, + headerTitle: headerTitle + ) + } +} + +struct CryptomatorErrorWithRefreshView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorErrorWithRefreshView(headerTitle: "Example Header Title", onRefresh: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 5a3d4fadf..96e8132da 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Combine import CryptomatorCloudAccessCore import Foundation import PCloudSDKSwift @@ -30,11 +31,13 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating static var cachedProvider = [CachedProvider]() public static let shared = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager.shared) let accountManager: CloudProviderAccountDBManager + let driveManager: MicrosoftGraphDriveManaging private let maxPageSizeForFileProvider = 500 - init(accountManager: CloudProviderAccountDBManager) { + init(accountManager: CloudProviderAccountDBManager, driveManager: MicrosoftGraphDriveManaging = MicrosoftGraphDriveManager.shared) { self.accountManager = accountManager + self.driveManager = driveManager } public func getProvider(with accountUID: String) throws -> CloudProvider { @@ -78,8 +81,8 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating } provider = try LocalFileSystemProvider(rootURL: rootURL, maxPageSize: .max) case .oneDrive: - let credential = try OneDriveCredential(with: accountUID) - provider = try OneDriveCloudProvider(credential: credential, maxPageSize: .max) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountUID) + provider = try MicrosoftGraphCloudProvider(credential: credential, maxPageSize: .max) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createClient(with: credential.user) @@ -87,6 +90,13 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .s3: let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider(credential: credential) + case .sharePoint: + let allDrives = try driveManager.getDrivesFromKeychain(for: accountUID) + guard let drive = allDrives.first else { + throw CloudProviderError.itemNotFound + } + let (credential, driveID) = try getSharePointCredentialAndDriveIdentifier(for: accountUID, driveID: drive.identifier) + provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: driveID, maxPageSize: .max) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient(credential: credential) @@ -129,8 +139,8 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating } provider = try LocalFileSystemProvider(rootURL: rootURL, maxPageSize: maxPageSizeForFileProvider) case .oneDrive: - let credential = try OneDriveCredential(with: accountUID) - provider = try OneDriveCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountUID) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createBackgroundClient(with: credential.user, sessionIdentifier: sessionIdentifier) @@ -138,6 +148,13 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .s3: let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider.withBackgroundSession(credential: credential, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + case .sharePoint: + let allDrives = try driveManager.getDrivesFromKeychain(for: accountUID) + guard let drive = allDrives.first else { + throw CloudProviderError.itemNotFound + } + let (credential, driveID) = try getSharePointCredentialAndDriveIdentifier(for: accountUID, driveID: drive.identifier) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, driveIdentifier: driveID, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient.withBackgroundSession(credential: credential, sessionIdentifier: sessionIdentifier, sharedContainerIdentifier: CryptomatorConstants.appGroupName) @@ -153,6 +170,14 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating return provider } + private func getSharePointCredentialAndDriveIdentifier(for accountUID: String, driveID: String) throws -> (MicrosoftGraphCredential, String) { + guard let drive = try driveManager.getDriveFromKeychain(for: accountUID, driveID: driveID) else { + throw CloudProviderError.itemNotFound + } + let credential = MicrosoftGraphCredential(identifier: drive.identifier, scopes: MicrosoftGraphScopes.sharePoint) + return (credential, drive.identifier) + } + private func getS3Credential(for accountUID: String) throws -> S3Credential { guard let credential = S3CredentialManager.shared.getCredential(with: accountUID) else { throw CloudProviderAccountError.accountNotFoundError diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift index 7ebdb1a5c..1c8fe6266 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift @@ -15,6 +15,7 @@ public enum CloudProviderType: Codable, Equatable, Hashable { case googleDrive case localFileSystem(type: LocalFileSystemType) case oneDrive + case sharePoint case pCloud case s3(type: S3Type) case webDAV(type: WebDAVType) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift new file mode 100644 index 000000000..b9bb03fa4 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift @@ -0,0 +1,90 @@ +// +// MicrosoftGraphDriveManager.swift +// CryptomatorCommon +// +// Created by Majid Achhoud on 07.01.25. +// + +import Foundation +import CryptomatorCloudAccessCore +import Combine + +public enum MicrosoftGraphDriveManagerError: Error { + case driveDuplicate(existingIdentifier: String) +} + +public protocol MicrosoftGraphDriveManaging { + var didUpdate: AnyPublisher { get } + + func getDrivesFromKeychain(for accountUID: String) throws -> [MicrosoftGraphDrive] + func saveDriveToKeychain(_ drive: MicrosoftGraphDrive, for accountUID: String) throws + func removeDriveFromKeychain(with accountUID: String, driveIdentifier: String) throws + func getDriveFromKeychain(for accountUID: String, driveID: String) throws -> MicrosoftGraphDrive? +} + +public struct MicrosoftGraphDriveManager: MicrosoftGraphDriveManaging { + public static let shared = MicrosoftGraphDriveManager(keychain: CryptomatorKeychain.microsoftGraph) + + public var didUpdate: AnyPublisher { + didUpdatePublisher.eraseToAnyPublisher() + } + + private let keychain: CryptomatorKeychainType + private let didUpdatePublisher = PassthroughSubject() + + public func getDrivesFromKeychain(for accountUID: String) throws -> [MicrosoftGraphDrive] { + return try keychain.getDriveIdentifiers(for: accountUID) ?? [] + } + + public func saveDriveToKeychain(_ drive: MicrosoftGraphDrive, for accountUID: String) throws { + var allDrives = try getDrivesFromKeychain(for: accountUID) + + if allDrives.contains(where: { $0.identifier == drive.identifier }) { + throw MicrosoftGraphDriveManagerError.driveDuplicate(existingIdentifier: drive.identifier) + } + + allDrives.append(drive) + try keychain.setDriveIdentifiers(allDrives, for: accountUID) + didUpdatePublisher.send(()) + } + + public func removeDriveFromKeychain(with accountUID: String, driveIdentifier: String) throws { + var allDrives = try getDrivesFromKeychain(for: accountUID) + allDrives.removeAll { $0.identifier == driveIdentifier } + try keychain.setDriveIdentifiers(allDrives, for: accountUID) + didUpdatePublisher.send(()) + } + + public func getDriveFromKeychain(for accountUID: String, driveID: String) throws -> MicrosoftGraphDrive? { + let allDrives = try getDrivesFromKeychain(for: accountUID) + return allDrives.first(where: { $0.identifier == driveID }) + } +} + +extension CryptomatorKeychainType { + func getDriveIdentifiers(for accountUID: String) throws -> [MicrosoftGraphDrive]? { + let driveKey = "driveIdentifiers_\(accountUID)" + guard let data = getAsData(driveKey) else { + return nil + } + do { + let jsonDecoder = JSONDecoder() + let drives = try jsonDecoder.decode([MicrosoftGraphDrive].self, from: data) + return drives + } catch { + return nil + } + } + + func setDriveIdentifiers(_ drives: [MicrosoftGraphDrive], for accountUID: String) throws { + let driveKey = "driveIdentifiers_\(accountUID)" + let jsonEncoder = JSONEncoder() + let encodedDrives = try jsonEncoder.encode(drives) + try set(driveKey, value: encodedDrives) + } + + func getDrive(by driveID: String, for accountUID: String) throws -> MicrosoftGraphDrive? { + let allDrives = try getDriveIdentifiers(for: accountUID) ?? [] + return allDrives.first(where: { $0.identifier == driveID }) + } +} diff --git a/CryptomatorIntents/SaveFileIntentHandler.swift b/CryptomatorIntents/SaveFileIntentHandler.swift index d2a8dc37d..eb9f90432 100644 --- a/CryptomatorIntents/SaveFileIntentHandler.swift +++ b/CryptomatorIntents/SaveFileIntentHandler.swift @@ -133,6 +133,8 @@ extension CloudProviderType { return type.assetName case .oneDrive: return "onedrive-vault" + case .sharePoint: + return "sharepoint-vault" case .pCloud: return "pcloud-vault" case .s3: diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index def01d6d2..484f97945 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -30,10 +30,10 @@ class FileProviderExtension: NSFileProviderExtension { FileProviderExtension.sharedDatabaseInitialized = true DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: CryptomatorConstants.appGroupName) PCloudSetup.constants = PCloudSetup(appKey: CloudAccessSecrets.pCloudAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName) BoxSetup.constants = BoxSetup(clientId: CloudAccessSecrets.boxClientId, clientSecret: CloudAccessSecrets.boxClientSecret, sharedContainerIdentifier: CryptomatorConstants.appGroupName) } catch { diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 2df85a802..ca9301811 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -64,10 +64,10 @@ class RootViewController: FPUIActionExtensionViewController { DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: nil) do { - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: nil) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: nil) } catch { DDLogError("Setting up OneDrive failed with error: \(error)") } diff --git a/README.md b/README.md index d68fa6649..dc3270140 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ export BOX_CLIENT_SECRET=... export DROPBOX_APP_KEY=... export GOOGLE_DRIVE_CLIENT_ID=... export GOOGLE_DRIVE_REDIRECT_URL_SCHEME=... -export ONEDRIVE_CLIENT_ID=... -export ONEDRIVE_REDIRECT_URI_SCHEME=... +export MICROSOFT_GRAPH_CLIENT_ID=... +export MICROSOFT_GRAPH_REDIRECT_URI_SCHEME=... export PCLOUD_APP_KEY=... ``` diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json new file mode 100644 index 000000000..82caa1afe --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint-vault-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint-vault-selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint-vault-selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png new file mode 100644 index 000000000..cfd9b460c Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png new file mode 100644 index 000000000..2a90e609f Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png new file mode 100644 index 000000000..c9d1ef12e Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json new file mode 100644 index 000000000..8da75771a --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint-vault.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint-vault@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint-vault@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png new file mode 100644 index 000000000..1763f39f8 Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@2x.png b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@2x.png new file mode 100644 index 000000000..c382ec99a Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@2x.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png new file mode 100644 index 000000000..f01cff15e Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json new file mode 100644 index 000000000..f59632c0d --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png new file mode 100644 index 000000000..2f796b363 Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@2x.png b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@2x.png new file mode 100644 index 000000000..9ae65b87c Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@2x.png differ diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@3x.png b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@3x.png new file mode 100644 index 000000000..a6946ba76 Binary files /dev/null and b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@3x.png differ diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 6445a8fd8..f45ae3b66 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -56,6 +56,13 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "Passwords do not match."; "addVault.createNewVault.password.error.tooShortPassword" = "Password must contain at least 8 characters."; "addVault.createNewVault.progress" = "Creating Vault…"; + +"addVault.enterSharePointURL.title" = "Enter SharePoint URL"; +"addVault.enterSharePointURL.placeholder" = "SharePoint Site URL"; +"addVault.enterSharePointURL.header.title" = "Enter the URL of the SharePoint Site."; +"addVault.enterSharePointURL.error.emptyURL" = "The URL cannot be empty."; +"addVault.enterSharePointURL.error.invalidURL" = "The entered URL is invalid. Please use the format https://{companyname}.sharepoint.com/sites/{SiteName}."; + "addVault.openExistingVault.title" = "Open Existing Vault"; "addVault.openExistingVault.chooseCloud.header" = "Where is the vault located?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detected the vault \"%@\".\nWould you like to add this vault?"; @@ -63,6 +70,10 @@ "addVault.openExistingVault.downloadVault.progress" = "Downloading Vault…"; "addVault.openExistingVault.password.footer" = "Enter password for \"%@\"."; "addVault.openExistingVault.progress" = "Adding Vault…"; + +"addVault.selectDrive.navigation.title" = "Select Drive"; +"addVault.selectDrive.header.description" = "Select the SharePoint drive where the encrypted vault will be located."; + "addVault.success.info" = "Successfully added vault \"%@\".\nAccess this vault via the Files app."; "addVault.success.footer" = "If you haven't already, enable Cryptomator in the Files app."; @@ -116,16 +127,17 @@ "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; "hubAuthentication.title" = "Hub Vault"; -"hubAuthentication.accessNotGranted" = "Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it."; +"hubAuthentication.accessNotGranted" = "Your user has not yet been authorized to access this vault. Ask the vault owner to authorize it."; "hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; "hubAuthentication.deviceRegistration.accountKey.footer.title" = "Your Account Key is required to login from new apps or browsers. It can be found in your profile."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; -"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault, your device needs to be authorized by the vault owner."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault"; "hubAuthentication.requireAccountInit.alert.title" = "Action Required"; "hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; "hubAuthentication.requireAccountInit.alert.actionButton" = "Go to Profile"; +"hubAuthentication.vaultArchived" = "This vault has been archived. Please ask the vault owner to unarchive it."; "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; diff --git a/fastlane/scripts/create-cloud-access-secrets.sh b/fastlane/scripts/create-cloud-access-secrets.sh index 26af91b77..f243cf966 100755 --- a/fastlane/scripts/create-cloud-access-secrets.sh +++ b/fastlane/scripts/create-cloud-access-secrets.sh @@ -27,9 +27,9 @@ public enum CloudAccessSecrets { public static let googleDriveClientId = "${GOOGLE_DRIVE_CLIENT_ID}" public static let googleDriveRedirectURLScheme = "${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}" public static let googleDriveRedirectURL = URL(string: "${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}:/oauthredirect") - public static let oneDriveClientId = "${ONEDRIVE_CLIENT_ID}" - public static let oneDriveRedirectURIScheme = "${ONEDRIVE_REDIRECT_URI_SCHEME}" - public static let oneDriveRedirectURI = "${ONEDRIVE_REDIRECT_URI_SCHEME}://auth" + public static let microsoftGraphClientId = "${MICROSOFT_GRAPH_CLIENT_ID}" + public static let microsoftGraphRedirectURIScheme = "${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}" + public static let microsoftGraphRedirectURI = "${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}://auth" public static let pCloudAppKey = "${PCLOUD_APP_KEY}" } EOM