diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 1d2f70b2b..8cef3bcf3 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -389,6 +389,7 @@ 740D3684266A1B180058744D /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740D3683266A1B180058744D /* SettingsCoordinator.swift */; }; 742679FC26A56CF9004C61BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 742679F926A56B33004C61BC /* Localizable.strings */; }; 742679FD26A56CFA004C61BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 742679F926A56B33004C61BC /* Localizable.strings */; }; + 743D95FC2D76EE9F002D73C3 /* SharePointAuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743D95FB2D76EE9A002D73C3 /* SharePointAuthenticationCoordinator.swift */; }; 74420BC32BD2449900E77F92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 74420BC22BD2449900E77F92 /* PrivacyInfo.xcprivacy */; }; 74420BC42BD2449900E77F92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 74420BC22BD2449900E77F92 /* PrivacyInfo.xcprivacy */; }; 74420BC52BD2449900E77F92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 74420BC22BD2449900E77F92 /* PrivacyInfo.xcprivacy */; }; @@ -397,6 +398,9 @@ 746815462475605E00038679 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AE97DB324572E4A00452814 /* Assets.xcassets */; }; 746815472475605E00038679 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AE97DB524572E4A00452814 /* LaunchScreen.storyboard */; }; 7469AD9A266E26B0000DCD45 /* URL+Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7469AD99266E26B0000DCD45 /* URL+Zip.swift */; }; + 7478BA452D81800C00BD3250 /* OneDriveAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7478BA442D81800900BD3250 /* OneDriveAuthenticator.swift */; }; + 7478BA472D8181F300BD3250 /* SharePointCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7478BA462D8181EF00BD3250 /* SharePointCredential.swift */; }; + 7478BA4B2D82EB5C00BD3250 /* SharePointURLValidationError+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7478BA4A2D82EB4C00BD3250 /* SharePointURLValidationError+Localization.swift */; }; 747C35172762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 747C35162762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift */; }; 747F2EB52587B7780072FB30 /* libCryptomatorFileProvider.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 740375D72587AE7A0023FF53 /* libCryptomatorFileProvider.a */; }; 747F2F1F2587BC250072FB30 /* FileProviderNotificator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740375F02587AEB40023FF53 /* FileProviderNotificator.swift */; }; @@ -420,6 +424,8 @@ 747F2F3B2587BC4B0072FB30 /* FileSystemLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740375FF2587AEB60023FF53 /* FileSystemLock.swift */; }; 747F2F3C2587BC4B0072FB30 /* RWLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740375FE2587AEB60023FF53 /* RWLock.swift */; }; 747F2F3D2587BC4B0072FB30 /* LockNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740376002587AEB60023FF53 /* LockNode.swift */; }; + 74A295ED2D80869700C54136 /* MicrosoftGraphAuthenticatorError+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A295EC2D80869700C54136 /* MicrosoftGraphAuthenticatorError+Localization.swift */; }; + 74A295EF2D80902800C54136 /* SharePointAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A295EE2D80902500C54136 /* SharePointAuthenticator.swift */; }; 74C2BC4826E8E21D00BCAA03 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C2BC4726E8E21D00BCAA03 /* OnboardingViewController.swift */; }; 74C2BC4A26E8E24A00BCAA03 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C2BC4926E8E24A00BCAA03 /* OnboardingViewModel.swift */; }; 74C2BC4C26E8E63700BCAA03 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C2BC4B26E8E63700BCAA03 /* OnboardingCoordinator.swift */; }; @@ -433,6 +439,11 @@ 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 /* SharePointAuthenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointAuthenticating.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 */ @@ -998,11 +1009,15 @@ 74275AE728478E160058AD25 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Intents.strings; sourceTree = ""; }; 74397A842832A05E00CB9410 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 74397A852832A09B00CB9410 /* sw-TZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sw-TZ"; path = "sw-TZ.lproj/Localizable.strings"; sourceTree = ""; }; + 743D95FB2D76EE9A002D73C3 /* SharePointAuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointAuthenticationCoordinator.swift; sourceTree = ""; }; 74420BC22BD2449900E77F92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 7460FFED26FB6C100018BCC4 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 7460FFEE26FCC6FC0018BCC4 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; 74626665283BD2D20070924B /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; + 7478BA442D81800900BD3250 /* OneDriveAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneDriveAuthenticator.swift; sourceTree = ""; }; + 7478BA462D8181EF00BD3250 /* SharePointCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointCredential.swift; sourceTree = ""; }; + 7478BA4A2D82EB4C00BD3250 /* SharePointURLValidationError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SharePointURLValidationError+Localization.swift"; sourceTree = ""; }; 747C35162762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTextHeaderFooterViewModel.swift; sourceTree = ""; }; 74833F9D27E4CCD800C1C5F0 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; 748BF2062B571AE7006304AD /* ba */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ba; path = ba.lproj/Localizable.strings; sourceTree = ""; }; @@ -1016,6 +1031,8 @@ 749D912C2C9308DD00FAD1A4 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Intents.strings; sourceTree = ""; }; 749D912D2C9308DE00FAD1A4 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Intents.strings; sourceTree = ""; }; 74A1B13D2726A9E60098224B /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; + 74A295EC2D80869700C54136 /* MicrosoftGraphAuthenticatorError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MicrosoftGraphAuthenticatorError+Localization.swift"; sourceTree = ""; }; + 74A295EE2D80902500C54136 /* SharePointAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointAuthenticator.swift; sourceTree = ""; }; 74AE94EF27A0282300D71AEC /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 74AE94F027A0283500D71AEC /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 74AE94F127A0285400D71AEC /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; @@ -1040,6 +1057,11 @@ 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 = ""; }; + 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 /* SharePointAuthenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointAuthenticating.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 */ @@ -1385,9 +1407,9 @@ 4A53CC14267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift */, 4A1EB0CF2689C7F8006D072B /* DetectedVaultFailureView.swift */, 4A1EB0CD2689C7A3006D072B /* DetectedVaultFailureViewController.swift */, - 4A644B4E267B9E6A008CBB9A /* VaultNaming.swift */, 4A644B43267A3BEC008CBB9A /* SetVaultNameViewController.swift */, 4A644B46267A3D43008CBB9A /* SetVaultNameViewModel.swift */, + 4A644B4E267B9E6A008CBB9A /* VaultNaming.swift */, 4A1EB0D3268A5A44006D072B /* LocalVault */, ); path = CreateNewVault; @@ -1487,7 +1509,6 @@ 4A8195DB25ADB8AD00F7DDA1 /* VaultList */ = { isa = PBXGroup; children = ( - 4A2FD04325B1C3BB008565C8 /* EmptyListMessage.swift */, 74FC576025ADED030003ED27 /* VaultCell.swift */, 4A6CF7FF27428CCB0061380A /* VaultCellViewModel.swift */, 4AF91CD825A722A600ACF01E /* VaultInfo.swift */, @@ -1513,6 +1534,7 @@ 4A8A6423286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift */, 4A512D69274277FF00DC26F8 /* EditableDataSource.swift */, 4AFCE4DC25B8514F0069C4FC /* EditableTableViewHeader.swift */, + 4A2FD04325B1C3BB008565C8 /* EmptyListMessage.swift */, 74D365B9268B5DB0005ECD69 /* FilesAppUtil.swift */, 4A63E4662742A8CB00026989 /* ListViewController.swift */, 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */, @@ -1804,15 +1826,16 @@ 4A7BC0E825ADF13100F007B3 /* AddVault */, 4A8195E325ADB92600F7DDA1 /* Common */, 4AE97DB524572E4A00452814 /* LaunchScreen.storyboard */, + 743D95FA2D76EE88002D73C3 /* MicrosoftGraph */, 74C2BC4626E8E1FD00BCAA03 /* Onboarding */, 74F5DC1A26DCD2E300AFE989 /* Purchase */, 7408E6C8267797DC00D7FAEA /* Resources */, + 4AED9A6A286B303500352951 /* S3 */, 740D367C266A18C80058744D /* Settings */, 4A13613027676F610077EB7F /* Snapshots */, 4A4B7E4026B2ABC4009BFDB1 /* VaultDetail */, 4A8195DB25ADB8AD00F7DDA1 /* VaultList */, 4AA22C08261CA71300A17486 /* WebDAV */, - 4AED9A6A286B303500352951 /* S3 */, ); path = Cryptomator; sourceTree = ""; @@ -2017,6 +2040,24 @@ path = Settings; sourceTree = ""; }; + 743D95FA2D76EE88002D73C3 /* MicrosoftGraph */ = { + isa = PBXGroup; + children = ( + B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */, + B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */, + 74A295EC2D80869700C54136 /* MicrosoftGraphAuthenticatorError+Localization.swift */, + 7478BA442D81800900BD3250 /* OneDriveAuthenticator.swift */, + B34C53292D142B9200F30FE9 /* SharePointAuthenticating.swift */, + 743D95FB2D76EE9A002D73C3 /* SharePointAuthenticationCoordinator.swift */, + 74A295EE2D80902500C54136 /* SharePointAuthenticator.swift */, + 7478BA462D8181EF00BD3250 /* SharePointCredential.swift */, + B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */, + B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */, + 7478BA4A2D82EB4C00BD3250 /* SharePointURLValidationError+Localization.swift */, + ); + path = MicrosoftGraph; + sourceTree = ""; + }; 74C2BC4626E8E1FD00BCAA03 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -2482,7 +2523,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; @@ -2701,6 +2742,7 @@ 4A773907286D86C20006B3C3 /* S3AuthenticationViewModel.swift in Sources */, 4A970FF4286C960E00337FDC /* S3CredentialVerifier.swift in Sources */, 4A74337A28B3E3AB00AECD21 /* WebDAVAuthentication.swift in Sources */, + 7478BA472D8181F300BD3250 /* SharePointCredential.swift in Sources */, 4A644B4F267B9E6A008CBB9A /* VaultNaming.swift in Sources */, 4A4B7E7726B95576009BFDB1 /* BaseHeaderFooterView.swift in Sources */, 4A9D124F261F071F00A670E2 /* WebDAVAuthenticator+VC.swift in Sources */, @@ -2718,6 +2760,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 */, @@ -2730,6 +2773,7 @@ 4A3D655F268099F9000DA764 /* VaultCoordinatorError.swift in Sources */, 4AF22C2627D8DE6500779802 /* NSAttributedString+Extension.swift in Sources */, 4AF4535D27205F6200CF1919 /* VaultDetailCoordinator.swift in Sources */, + 743D95FC2D76EE9F002D73C3 /* SharePointAuthenticationCoordinator.swift in Sources */, 4AB52338275F7AB0009B8D99 /* LoadingButtonCellViewModel.swift in Sources */, 4A1EB0CC2689C3DE006D072B /* CreateNewLocalVaultCoordinator.swift in Sources */, 4A644B57267C958F008CBB9A /* ChildCoordinator.swift in Sources */, @@ -2760,6 +2804,7 @@ 4AFCE53A25B9D6A60069C4FC /* CloudAuthenticator.swift in Sources */, 4A9D1247261E227600A670E2 /* WebDAVAuthenticationCoordinator.swift in Sources */, 4A644B55267C926A008CBB9A /* FolderCreating.swift in Sources */, + 74A295EF2D80902800C54136 /* SharePointAuthenticator.swift in Sources */, 4A3D658626847B11000DA764 /* CreateNewLocalVaultViewModel.swift in Sources */, 4AE97DAB24572E4900452814 /* AppDelegate.swift in Sources */, 4AA22C16261CA8D800A17486 /* URLFieldCell.swift in Sources */, @@ -2775,6 +2820,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 */, @@ -2783,6 +2829,7 @@ 4A1673E3270C4DD90075C724 /* LoadingWithLabelCellViewModel.swift in Sources */, 4AFCE4DD25B8514F0069C4FC /* EditableTableViewHeader.swift in Sources */, 4A2FD07025B5D5FB008565C8 /* ChooseCloudViewController.swift in Sources */, + 7478BA452D81800C00BD3250 /* OneDriveAuthenticator.swift in Sources */, 4A1EB0CA2689C373006D072B /* LocalVaultAdding.swift in Sources */, 4A4B7E6D26B9462F009BFDB1 /* Bindable.swift in Sources */, 4A512D6A274277FF00DC26F8 /* EditableDataSource.swift in Sources */, @@ -2790,6 +2837,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,6 +2868,7 @@ 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 */, @@ -2831,6 +2880,7 @@ 4A8A6424286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift in Sources */, 4AB1D4FF27D69C9A009060AB /* DisclosureCell.swift in Sources */, 4A7077FF278DC2ED00AEF4CE /* VaultKeepUnlockedViewController.swift in Sources */, + B34C532A2D142BA700F30FE9 /* SharePointAuthenticating.swift in Sources */, 4A21B49C26BD68C2000D13DF /* UIControl+Publisher.swift in Sources */, 4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */, 4A4B7E4A26B2C071009BFDB1 /* ButtonCellViewModel.swift in Sources */, @@ -2863,7 +2913,9 @@ 4A88816427440CE300F7AA6E /* BaseUITableViewController.swift in Sources */, 4A2745E228475F3500E70D5F /* Intents.intentdefinition in Sources */, 4AF91CE225A7234500ACF01E /* DatabaseManager.swift in Sources */, + 74A295ED2D80869700C54136 /* MicrosoftGraphAuthenticatorError+Localization.swift in Sources */, 4A3C5DDA272BF52600EB7C7A /* TextFieldCellViewModel.swift in Sources */, + 7478BA4B2D82EB5C00BD3250 /* SharePointURLValidationError+Localization.swift in Sources */, 4AFCE51F25B89CD80069C4FC /* CloudProviderType+Localization.swift in Sources */, 4A447E0425BF0B0F00D9520D /* SingleSectionTableViewController.swift in Sources */, 4A3D65612680A3CB000DA764 /* LocalFileSystemAuthenticating.swift in Sources */, diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5a4425aba..c914a9eb3 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", - "version" : "1.7.5" + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/leif-ibsen/ASN1", "state" : { - "revision" : "4b4e82513e3b4d51a7573972fd7123222dd3a3bd", - "version" : "2.6.0" + "revision" : "e38d1b8b43d8b53ffadde9836f34289176bb7a0c", + "version" : "2.7.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/leif-ibsen/BigInt", "state" : { - "revision" : "bf55e4ce076a5e2dde0db13d9b03d820cfad420d", - "version" : "1.19.0" + "revision" : "afb70a0038bfbba845271b60fa9a58d5840f8017", + "version" : "1.21.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cryptomator/cloud-access-swift.git", "state" : { - "revision" : "299be2306bc133b6eefbf18c172a1b5ed9808a44", - "version" : "1.12.1" + "revision" : "1ef0aa8c3afc5805cb76fcd05630822e2b70a626", + "version" : "2.0.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/leif-ibsen/Digest", "state" : { - "revision" : "844a17be22efaa443130d081f2c4fa5f12c68e91", - "version" : "1.8.0" + "revision" : "1b9858026d5d3cb7b371fb1683ece54a654dfb21", + "version" : "1.11.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state" : { - "revision" : "b8733236bfd16e10849f4752a4d1c4621e4bf930", - "version" : "1.5.0" + "revision" : "bad310566a9f86cc5f41b0a4e29618f7f7d5d5f6", + "version" : "1.5.1" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pCloud/pcloud-sdk-swift.git", "state" : { - "revision" : "ad1a7d8b3a59f12185d7bc89ff7a1b8c087ed0c0", - "version" : "3.2.2" + "revision" : "cc81e0250a9f378019470c78ce9a8bb501dcaeda", + "version" : "3.2.3" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log", "state" : { - "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", - "version" : "1.6.1" + "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", + "version" : "1.6.3" } }, { @@ -248,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "bc2a151366f2cd0e347274544933bc2acb00c9fe", - "version" : "1.4.0" + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" } } ], diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift index 313937f16..43acc6cc2 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultChooseFolderViewController.swift @@ -72,6 +72,8 @@ private class CreateNewVaultChooseFolderViewModelMock: ChooseFolderViewModelProt } func refreshItems() {} + + func addItem(_ item: CloudItemMetadata) {} } struct CreateNewVaultChooseFolderVCPreview: PreviewProvider { diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index 3f34fc168..8a6134cc5 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -24,7 +24,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, .microsoftGraph(type: .oneDrive), .microsoftGraph(type: .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 @@ -47,6 +47,11 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA authenticator.authenticate(cloudProviderType, from: viewController).then { account in let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) self.startFolderChooser(with: provider, account: account) + }.catch { error in + guard case CocoaError.userCancelled = error else { + self.handleError(error, for: self.navigationController) + return + } } } @@ -135,10 +140,11 @@ private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInsta navigationController.pushViewController(passwordVC, animated: true) } - func showCreateNewFolder(parentPath: CloudPath) { + func showCreateNewFolder(parentPath: CloudPath, delegate: ChooseFolderViewModelProtocol?) { let modalNavigationController = BaseNavigationController() let child = AuthenticatedFolderCreationCoordinator(navigationController: modalNavigationController, provider: provider, parentPath: parentPath) child.parentCoordinator = self + child.delegate = delegate childCoordinators.append(child) navigationController.topViewController?.present(modalNavigationController, animated: true) child.start() @@ -163,6 +169,7 @@ private class AuthenticatedCreateNewVaultCoordinator: FolderChoosing, VaultInsta class AuthenticatedFolderCreationCoordinator: FolderCreating, ChildCoordinator { weak var parentCoordinator: Coordinator? + weak var delegate: ChooseFolderViewModelProtocol? var childCoordinators = [Coordinator]() var navigationController: UINavigationController private let provider: CloudProvider @@ -183,6 +190,7 @@ class AuthenticatedFolderCreationCoordinator: FolderCreating, ChildCoordinator { func createdNewFolder(at folderPath: CloudPath) { navigationController.dismiss(animated: true) + delegate?.addItem(CloudItemMetadata(name: folderPath.lastPathComponent, cloudPath: folderPath, itemType: .folder, lastModifiedDate: nil, size: nil)) if let folderChoosingParentCoordinator = parentCoordinator as? FolderChoosing { folderChoosingParentCoordinator.showItems(for: folderPath) } diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift index 725616185..35abf14f4 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift @@ -102,6 +102,8 @@ private class OpenExistingVaultChooseFolderViewModelMock: ChooseFolderViewModelP } func refreshItems() {} + + func addItem(_ item: CloudItemMetadata) {} } struct OpenExistingVaultChooseFolderVCPreview: PreviewProvider { diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index cb5a01c17..7c7660df1 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -25,7 +25,7 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } 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, .microsoftGraph(type: .oneDrive), .microsoftGraph(type: .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 @@ -48,6 +48,11 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd authenticator.authenticate(cloudProviderType, from: viewController).then { account in let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) self.startFolderChooser(with: provider, account: account) + }.catch { error in + guard case CocoaError.userCancelled = error else { + self.handleError(error, for: self.navigationController) + return + } } } @@ -208,7 +213,7 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder child.start() } - func showCreateNewFolder(parentPath: CloudPath) {} + func showCreateNewFolder(parentPath: CloudPath, delegate: ChooseFolderViewModelProtocol?) {} func handleError(error: Error) { navigationController.popViewController(animated: true) diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 578578501..4b5d5e8c6 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)") } @@ -80,14 +80,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let credential = DropboxCredential(tokenUID: tokenUid) DropboxAuthenticator.pendingAuthentication?.fulfill(credential) } else if authResult.isCancel() { - DropboxAuthenticator.pendingAuthentication?.reject(DropboxAuthenticatorError.userCanceled) + DropboxAuthenticator.pendingAuthentication?.reject(CocoaError(.userCancelled)) } else if authResult.isError() { DropboxAuthenticator.pendingAuthentication?.reject(authResult.nsError) } } } 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/ChooseFolder/ChooseFolderViewController.swift b/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift index 3ea8b24fe..0075f8a0c 100644 --- a/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift +++ b/Cryptomator/Common/ChooseFolder/ChooseFolderViewController.swift @@ -81,7 +81,7 @@ class ChooseFolderViewController: SingleSectionTableViewController { } @objc func createNewFolder() { - coordinator?.showCreateNewFolder(parentPath: viewModel.cloudPath) + coordinator?.showCreateNewFolder(parentPath: viewModel.cloudPath, delegate: viewModel) } @objc func pullToRefresh() { @@ -205,6 +205,8 @@ private class ChooseFolderViewModelMock: ChooseFolderViewModelProtocol { } func refreshItems() {} + + func addItem(_ item: CloudItemMetadata) {} } struct ChooseFolderVCPreview: PreviewProvider { diff --git a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift index 66dc5dc0f..133f89af3 100644 --- a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift +++ b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift @@ -11,7 +11,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation -protocol ChooseFolderViewModelProtocol { +protocol ChooseFolderViewModelProtocol: AnyObject { var canCreateFolder: Bool { get } var cloudPath: CloudPath { get } var foundMasterkey: Bool { get } @@ -19,6 +19,7 @@ protocol ChooseFolderViewModelProtocol { var items: [CloudItemMetadata] { get } func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) func refreshItems() + func addItem(_ item: CloudItemMetadata) } class ChooseFolderViewModel: ChooseFolderViewModelProtocol { @@ -62,4 +63,10 @@ class ChooseFolderViewModel: ChooseFolderViewModelProtocol { self.errorListener?(error) } } + + func addItem(_ item: CloudItemMetadata) { + items.append(item) + items.sort { $0.name.localizedStandardCompare($1.name) == .orderedAscending } + changeListener?() + } } diff --git a/Cryptomator/Common/ChooseFolder/FolderChoosing.swift b/Cryptomator/Common/ChooseFolder/FolderChoosing.swift index d85b98c04..72ca4c8a9 100644 --- a/Cryptomator/Common/ChooseFolder/FolderChoosing.swift +++ b/Cryptomator/Common/ChooseFolder/FolderChoosing.swift @@ -12,7 +12,7 @@ protocol FolderChoosing: AnyObject { func showItems(for path: CloudPath) func close() func chooseItem(_ item: Item) - func showCreateNewFolder(parentPath: CloudPath) + func showCreateNewFolder(parentPath: CloudPath, delegate: ChooseFolderViewModelProtocol?) func handleError(error: Error) } diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index f32d81180..035b1d5d1 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, .microsoftGraph(type: .oneDrive), .microsoftGraph(type: .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..05eef4762 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift @@ -57,6 +57,15 @@ class AccountListViewModel: AccountListViewModelProtocol { } } + func refreshMicrosoftGraphItems() -> Promise { + return all(accountInfos + .compactMap { try? MicrosoftGraphAccountDBManager.shared.getAccount(for: $0.accountUID) } + .compactMap { try? self.createAccountCellContent(for: $0) } + ).then { accounts in + self.accounts = accounts + } + } + func refreshPCloudItems() -> Promise { return all(accountInfos .compactMap { try? PCloudCredential(userID: $0.accountUID) } @@ -86,9 +95,9 @@ class AccountListViewModel: AccountListViewModelProtocol { return try createAccountCellContent(for: credential) case .localFileSystem: throw AccountListError.unsupportedCloudProviderType - case .oneDrive: - let credential = try OneDriveCredential(with: accountInfo.accountUID) - return try createAccountCellContent(for: credential) + case .microsoftGraph: + let account = try MicrosoftGraphAccountDBManager.shared.getAccount(for: accountInfo.accountUID) + return try createAccountCellContentPlaceholder(for: account) case .pCloud: return createAccountCellContentPlaceholder() case .s3: @@ -120,9 +129,36 @@ class AccountListViewModel: AccountListViewModelProtocol { return AccountCellContent(mainLabelText: username, detailLabelText: nil) } - func createAccountCellContent(for credential: OneDriveCredential) throws -> AccountCellContent { + func createAccountCellContentPlaceholder(for account: MicrosoftGraphAccount) throws -> AccountCellContent { + let credential = MicrosoftGraphCredential(identifier: account.credentialID, type: account.type) let username = try credential.getUsername() - return AccountCellContent(mainLabelText: username, detailLabelText: nil) + var detailLabelTextComponents: [String] = [] + if let siteURL = account.siteURL?.absoluteString.replacingOccurrences(of: "https://", with: "") { + detailLabelTextComponents.append(siteURL) + } + if account.driveID != nil { + detailLabelTextComponents.append("(…)") + } + let detailLabelText = detailLabelTextComponents.joined(separator: " • ") + return AccountCellContent(mainLabelText: username, detailLabelText: detailLabelText) + } + + func createAccountCellContent(for account: MicrosoftGraphAccount) throws -> Promise { + guard let driveID = account.driveID else { + return try Promise(createAccountCellContentPlaceholder(for: account)) + } + let credential = MicrosoftGraphCredential(identifier: account.credentialID, type: account.type) + let username = try credential.getUsername() + let discovery = MicrosoftGraphDiscovery(credential: credential) + return discovery.fetchDrive(for: driveID).then { drive in + var detailLabelTextComponents: [String] = [] + if let siteURL = account.siteURL?.absoluteString.replacingOccurrences(of: "https://", with: "") { + detailLabelTextComponents.append(siteURL) + } + detailLabelTextComponents.append("\(drive.name ?? "")") + let detailLabelText = detailLabelTextComponents.joined(separator: " • ") + return AccountCellContent(mainLabelText: username, detailLabelText: detailLabelText) + } } func createAccountCellContent(for credential: PCloudCredential) -> Promise { @@ -176,6 +212,7 @@ class AccountListViewModel: AccountListViewModelProtocol { } } + // swiftlint:disable:next function_body_length func startListenForChanges() -> AnyPublisher, Never> { observation = dbManager.observeCloudProviderAccounts(onError: { error in DDLogError("Observe vault accounts failed with error: \(error)") @@ -202,24 +239,33 @@ class AccountListViewModel: AccountListViewModelProtocol { // Also fixes the problem that an empty account list is sent a second time via the `databaseChangedPublisher`. return } - if self.cloudProviderType == .dropbox { + switch self.cloudProviderType { + case .box: + self.refreshBoxItems().then { + self.databaseChangedPublisher.send(.success(self.accounts)) + }.catch { error in + self.databaseChangedPublisher.send(.failure(error)) + } + case .dropbox: self.refreshDropboxItems().then { self.databaseChangedPublisher.send(.success(self.accounts)) }.catch { error in self.databaseChangedPublisher.send(.failure(error)) } - } else if self.cloudProviderType == .pCloud { - self.refreshPCloudItems().then { + case .microsoftGraph: + self.refreshMicrosoftGraphItems().then { self.databaseChangedPublisher.send(.success(self.accounts)) }.catch { error in self.databaseChangedPublisher.send(.failure(error)) } - } else if self.cloudProviderType == .box { - self.refreshBoxItems().then { + case .pCloud: + self.refreshPCloudItems().then { self.databaseChangedPublisher.send(.success(self.accounts)) }.catch { error in self.databaseChangedPublisher.send(.failure(error)) } + default: + break } }) return databaseChangedPublisher.eraseToAnyPublisher() diff --git a/Cryptomator/Common/CloudAuthenticator.swift b/Cryptomator/Common/CloudAuthenticator.swift index 21258e0ef..6687fccde 100644 --- a/Cryptomator/Common/CloudAuthenticator.swift +++ b/Cryptomator/Common/CloudAuthenticator.swift @@ -18,11 +18,36 @@ class CloudAuthenticator { private let accountManager: CloudProviderAccountManager private let vaultManager: VaultManager private let vaultAccountManager: VaultAccountManager + private let microsoftGraphAccountManager: MicrosoftGraphAccountManager - init(accountManager: CloudProviderAccountManager, vaultManager: VaultManager = VaultDBManager.shared, vaultAccountManager: VaultAccountManager = VaultAccountDBManager.shared) { + init(accountManager: CloudProviderAccountManager, vaultManager: VaultManager = VaultDBManager.shared, vaultAccountManager: VaultAccountManager = VaultAccountDBManager.shared, microsoftGraphAccountManager: MicrosoftGraphAccountManager = MicrosoftGraphAccountDBManager.shared) { self.accountManager = accountManager self.vaultManager = vaultManager self.vaultAccountManager = vaultAccountManager + self.microsoftGraphAccountManager = microsoftGraphAccountManager + } + + func authenticate(_ cloudProviderType: CloudProviderType, from viewController: UIViewController) -> Promise { + switch cloudProviderType { + case .box: + return authenticateBox(from: viewController) + case .dropbox: + return authenticateDropbox(from: viewController) + case .googleDrive: + return authenticateGoogleDrive(from: viewController) + case .localFileSystem: + return Promise(CloudAuthenticatorError.functionNotYetSupported) + case .microsoftGraph(type: .oneDrive): + return authenticateOneDrive(from: viewController) + case .microsoftGraph(type: .sharePoint): + return authenticateSharePoint(from: viewController) + case .pCloud: + return authenticatePCloud(from: viewController) + case .s3: + return authenticateS3(from: viewController) + case .webDAV: + return authenticateWebDAV(from: viewController) + } } func authenticateDropbox(from viewController: UIViewController) -> Promise { @@ -44,11 +69,11 @@ class CloudAuthenticator { } func authenticateOneDrive(from viewController: UIViewController) -> Promise { - OneDriveAuthenticator.authenticate(from: viewController).then { credential -> CloudProviderAccount in - let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .oneDrive) - try self.accountManager.saveNewAccount(account) - return account - } + return OneDriveAuthenticator.authenticate(from: viewController, cloudProviderAccountManager: accountManager, microsoftGraphAccountManager: microsoftGraphAccountManager) + } + + func authenticateSharePoint(from viewController: UIViewController) -> Promise { + return SharePointAuthenticator.authenticate(from: viewController, cloudProviderAccountManager: accountManager, microsoftGraphAccountManager: microsoftGraphAccountManager) } func authenticatePCloud(from viewController: UIViewController) -> Promise { @@ -89,27 +114,6 @@ class CloudAuthenticator { } } - func authenticate(_ cloudProviderType: CloudProviderType, from viewController: UIViewController) -> Promise { - switch cloudProviderType { - case .box: - return authenticateBox(from: viewController) - case .dropbox: - return authenticateDropbox(from: viewController) - case .googleDrive: - return authenticateGoogleDrive(from: viewController) - case .localFileSystem: - return Promise(CloudAuthenticatorError.functionNotYetSupported) - case .oneDrive: - return authenticateOneDrive(from: viewController) - case .pCloud: - return authenticatePCloud(from: viewController) - case .s3: - return authenticateS3(from: viewController) - case .webDAV: - return authenticateWebDAV(from: viewController) - } - } - func deauthenticate(account: CloudProviderAccount) throws { switch account.cloudProviderType { case .box: @@ -124,9 +128,8 @@ class CloudAuthenticator { credential.deauthenticate() case .localFileSystem: break - case .oneDrive: - let credential = try OneDriveCredential(with: account.accountUID) - try credential.deauthenticate() + case let .microsoftGraph(type): + try deauthenticateMicrosoftGraph(account: account, type: type) case .pCloud: let credential = try PCloudCredential(userID: account.accountUID) try credential.deauthenticate() @@ -151,6 +154,16 @@ class CloudAuthenticator { DDLogError("Deauthenticate account: \(account) failed with error: \(error)") } } + + func deauthenticateMicrosoftGraph(account: CloudProviderAccount, type: MicrosoftGraphType) throws { + let microsoftGraphAccount = try microsoftGraphAccountManager.getAccount(for: account.accountUID) + if try microsoftGraphAccountManager.multipleAccountsExist(for: microsoftGraphAccount.credentialID) { + DDLogInfo("Skipped deauthentication for accountUID \(microsoftGraphAccount.accountUID) because the credentialID \(microsoftGraphAccount.credentialID) appears multiple times in the database.") + } else { + let credential = MicrosoftGraphCredential(identifier: microsoftGraphAccount.credentialID, type: type) + try credential.deauthenticate() + } + } } enum CloudAuthenticatorError: Error { diff --git a/Cryptomator/Common/CloudProviderType+Localization.swift b/Cryptomator/Common/CloudProviderType+Localization.swift index 79b2d8ba0..f1a3b9254 100644 --- a/Cryptomator/Common/CloudProviderType+Localization.swift +++ b/Cryptomator/Common/CloudProviderType+Localization.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation @@ -18,10 +19,10 @@ extension CloudProviderType { return "Dropbox" case .googleDrive: return "Google Drive" - case let .localFileSystem(localFileSystemType): - return localFileSystemType.localizedString() - case .oneDrive: - return "OneDrive" + case let .localFileSystem(type): + return type.localizedString() + case let .microsoftGraph(type): + return type.localizedString() case .pCloud: return "pCloud" case .s3: @@ -42,3 +43,14 @@ extension LocalFileSystemType { } } } + +extension MicrosoftGraphType { + func localizedString() -> String { + switch self { + case .oneDrive: + return "OneDrive" + case .sharePoint: + return "SharePoint" + } + } +} diff --git a/Cryptomator/VaultList/EmptyListMessage.swift b/Cryptomator/Common/EmptyListMessage.swift similarity index 100% rename from Cryptomator/VaultList/EmptyListMessage.swift rename to Cryptomator/Common/EmptyListMessage.swift diff --git a/Cryptomator/Common/UIImage+CloudProviderType.swift b/Cryptomator/Common/UIImage+CloudProviderType.swift index 4e2a4b09c..06a63e19b 100644 --- a/Cryptomator/Common/UIImage+CloudProviderType.swift +++ b/Cryptomator/Common/UIImage+CloudProviderType.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import CryptomatorCommonCore import UIKit @@ -24,10 +25,10 @@ extension UIImage { assetName = "dropbox-vault" case .googleDrive: assetName = "google-drive-vault" - case let .localFileSystem(localFileSystemType): - assetName = UIImage.getVaultIcon(for: localFileSystemType) - case .oneDrive: - assetName = "onedrive-vault" + case let .localFileSystem(type): + assetName = UIImage.getVaultIcon(for: type) + case let .microsoftGraph(type): + assetName = UIImage.getVaultIcon(for: type) case .pCloud: assetName = "pcloud-vault" case .s3: @@ -50,6 +51,15 @@ extension UIImage { } } + private static func getVaultIcon(for type: MicrosoftGraphType) -> String { + switch type { + case .oneDrive: + return "onedrive-vault" + case .sharePoint: + return "sharepoint-vault" + } + } + convenience init?(storageIconFor cloudProviderType: CloudProviderType) { var assetName: String switch cloudProviderType { @@ -59,10 +69,10 @@ extension UIImage { assetName = "dropbox" case .googleDrive: assetName = "google-drive" - case let .localFileSystem(localFileSystemType): - assetName = UIImage.getStorageIcon(for: localFileSystemType) - case .oneDrive: - assetName = "onedrive" + case let .localFileSystem(type): + assetName = UIImage.getStorageIcon(for: type) + case let .microsoftGraph(type): + assetName = UIImage.getStorageIcon(for: type) case .pCloud: assetName = "pcloud" case .s3: @@ -81,4 +91,13 @@ extension UIImage { return "icloud-drive" } } + + private static func getStorageIcon(for type: MicrosoftGraphType) -> String { + switch type { + case .oneDrive: + return "onedrive" + case .sharePoint: + return "sharepoint" + } + } } diff --git a/Cryptomator/MicrosoftGraph/EnterSharePointURLViewController.swift b/Cryptomator/MicrosoftGraph/EnterSharePointURLViewController.swift new file mode 100644 index 000000000..b06b704f9 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/EnterSharePointURLViewController.swift @@ -0,0 +1,61 @@ +// +//  EnterSharePointURLViewController.swift +//  Cryptomator +// +//  Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Combine +import CryptomatorCommonCore +import UIKit + +class EnterSharePointURLViewController: SingleSectionStaticUITableViewController { + weak var coordinator: (Coordinator & SharePointAuthenticating)? + 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 + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) + navigationItem.leftBarButtonItem = cancelButton + lastReturnButtonPressedSubscriber = viewModel.lastReturnButtonPressed.sink { [weak self] in + self?.lastReturnButtonPressedAction() + } + } + + @objc func nextButtonClicked() { + guard let coordinator = coordinator else { return } + do { + let siteURL = try viewModel.getValidatedSharePointURL() + coordinator.setSiteURL(siteURL, from: self) + } catch { + coordinator.handleError(error, for: self) + } + } + + @objc func cancel() { + coordinator?.cancel() + } + + func lastReturnButtonPressedAction() { + nextButtonClicked() + } +} + +#if DEBUG +import SwiftUI + +struct EnterSharePointURLVCPreview: PreviewProvider { + static var previews: some View { + EnterSharePointURLViewController(viewModel: EnterSharePointURLViewModel()).toPreview() + } +} +#endif diff --git a/Cryptomator/MicrosoftGraph/EnterSharePointURLViewModel.swift b/Cryptomator/MicrosoftGraph/EnterSharePointURLViewModel.swift new file mode 100644 index 000000000..5a778097c --- /dev/null +++ b/Cryptomator/MicrosoftGraph/EnterSharePointURLViewModel.swift @@ -0,0 +1,56 @@ +// +// EnterSharePointURLViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Combine +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +protocol EnterSharePointURLViewModelProtocol: SingleSectionTableViewModel, ReturnButtonSupport { + func getValidatedSharePointURL() throws -> URL +} + +class EnterSharePointURLViewModel: SingleSectionTableViewModel, EnterSharePointURLViewModelProtocol { + var lastReturnButtonPressed: AnyPublisher { + return setupReturnButtonSupport(for: [sharePointURLCellViewModel], subscribers: &subscribers) + } + + override var cells: [TableViewCellViewModel] { + return [sharePointURLCellViewModel] + } + + override var title: String? { + return LocalizedString.getValue("sharePoint.enterURL.title") + } + + let sharePointURLCellViewModel = TextFieldCellViewModel(type: .url, text: "https://", placeholder: LocalizedString.getValue("sharePoint.enterURL.placeholder"), isInitialFirstResponder: true) + + var trimmedSharePointURL: String { + var trimmedSharePointURL = sharePointURLCellViewModel.input.value.trimmingCharacters(in: .whitespacesAndNewlines) + while trimmedSharePointURL.hasSuffix("/") && trimmedSharePointURL.count > "https://".count { + trimmedSharePointURL = String(trimmedSharePointURL.dropLast()) + } + return trimmedSharePointURL + } + + private lazy var subscribers = Set() + + func getValidatedSharePointURL() throws -> URL { + guard let url = URL(string: trimmedSharePointURL) else { + throw SharePointURLValidationError.invalidURL + } + return try url.validateForSharePoint() + } + + override func getHeaderTitle(for section: Int) -> String? { + guard section == 0 else { + return nil + } + return LocalizedString.getValue("sharePoint.enterURL.header.title") + } +} diff --git a/Cryptomator/MicrosoftGraph/MicrosoftGraphAuthenticatorError+Localization.swift b/Cryptomator/MicrosoftGraph/MicrosoftGraphAuthenticatorError+Localization.swift new file mode 100644 index 000000000..21690d7be --- /dev/null +++ b/Cryptomator/MicrosoftGraph/MicrosoftGraphAuthenticatorError+Localization.swift @@ -0,0 +1,22 @@ +// +// MicrosoftGraphAuthenticatorError+Localization.swift +// CryptomatorCommon +// +// Created by Tobias Hagemann on 11.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccess +import CryptomatorCommonCore +import Foundation + +extension MicrosoftGraphAuthenticatorError: LocalizedError { + public var errorDescription: String? { + switch self { + case .missingAccountIdentifier: + return nil + case .serverDeclinedScopes: + return LocalizedString.getValue("microsoftGraphAuthenticator.error.serverDeclinedScopes") + } + } +} diff --git a/Cryptomator/MicrosoftGraph/OneDriveAuthenticator.swift b/Cryptomator/MicrosoftGraph/OneDriveAuthenticator.swift new file mode 100644 index 000000000..985a253ed --- /dev/null +++ b/Cryptomator/MicrosoftGraph/OneDriveAuthenticator.swift @@ -0,0 +1,35 @@ +// +// OneDriveAuthenticator.swift +// Cryptomator +// +// Created by Tobias Hagemann on 12.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccess +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import GRDB +import Promises +import UIKit + +public class OneDriveAuthenticator { + public static func authenticate(from viewController: UIViewController, cloudProviderAccountManager: CloudProviderAccountManager, microsoftGraphAccountManager: MicrosoftGraphAccountManager) -> Promise { + return MicrosoftGraphAuthenticator.authenticate(from: viewController, for: .oneDrive).then { credential -> CloudProviderAccount in + let accountUID = UUID().uuidString + let cloudProviderAccount = CloudProviderAccount(accountUID: accountUID, cloudProviderType: .microsoftGraph(type: .oneDrive)) + try cloudProviderAccountManager.saveNewAccount(cloudProviderAccount) // Make sure to save this first, because Microsoft Graph account has a reference to the Cloud Provider account. + do { + let microsoftGraphAccount = MicrosoftGraphAccount(accountUID: accountUID, credentialID: credential.identifier, type: .oneDrive) + try microsoftGraphAccountManager.saveNewAccount(microsoftGraphAccount) + return cloudProviderAccount + } catch let dbError as DatabaseError where dbError.resultCode == .SQLITE_CONSTRAINT { + try cloudProviderAccountManager.removeAccount(with: accountUID) + let existingMicrosoftGraphAccount = try microsoftGraphAccountManager.getAccount(credentialID: credential.identifier, driveID: nil, type: .oneDrive) + return try cloudProviderAccountManager.getAccount(for: existingMicrosoftGraphAccount.accountUID) + } catch { + throw error + } + } + } +} diff --git a/Cryptomator/MicrosoftGraph/SharePointAuthenticating.swift b/Cryptomator/MicrosoftGraph/SharePointAuthenticating.swift new file mode 100644 index 000000000..037d3cdf2 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointAuthenticating.swift @@ -0,0 +1,16 @@ +// +// SharePointAuthenticating.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import UIKit + +protocol SharePointAuthenticating: AnyObject { + func setSiteURL(_ siteURL: URL, from viewController: UIViewController) + func authenticated(_ credential: SharePointCredential) throws + func cancel() +} diff --git a/Cryptomator/MicrosoftGraph/SharePointAuthenticationCoordinator.swift b/Cryptomator/MicrosoftGraph/SharePointAuthenticationCoordinator.swift new file mode 100644 index 000000000..93c0cd8d9 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointAuthenticationCoordinator.swift @@ -0,0 +1,65 @@ +// +// SharePointAuthenticationCoordinator.swift +// Cryptomator +// +// Created by Tobias Hagemann on 04.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccess +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import GRDB +import Promises +import UIKit + +class SharePointAuthenticationCoordinator: Coordinator, SharePointAuthenticating { + let pendingAuthentication = Promise.pending() + var navigationController: UINavigationController + var childCoordinators = [Coordinator]() + weak var parentCoordinator: Coordinator? + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start() { + let viewModel = EnterSharePointURLViewModel() + let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel) + enterURLVC.coordinator = self + navigationController.pushViewController(enterURLVC, animated: true) + } + + func setSiteURL(_ siteURL: URL, from viewController: UIViewController) { + MicrosoftGraphAuthenticator.authenticate(from: viewController, for: .sharePoint).then { credential in + self.showDriveList(credential: credential, siteURL: siteURL) + }.catch { error in + guard case CocoaError.userCancelled = error else { + self.handleError(error, for: self.navigationController) + return + } + } + } + + private func showDriveList(credential: MicrosoftGraphCredential, siteURL: URL) { + let viewModel = SharePointDriveListViewModel(credential: credential, siteURL: siteURL) + let driveListVC = SharePointDriveListViewController(viewModel: viewModel) + driveListVC.coordinator = self + navigationController.pushViewController(driveListVC, animated: true) + } + + func authenticated(_ credential: SharePointCredential) throws { + pendingAuthentication.fulfill(credential) + close() + } + + func cancel() { + pendingAuthentication.reject(CocoaError(.userCancelled)) + close() + } + + private func close() { + navigationController.dismiss(animated: true) + parentCoordinator?.childDidFinish(self) + } +} diff --git a/Cryptomator/MicrosoftGraph/SharePointAuthenticator.swift b/Cryptomator/MicrosoftGraph/SharePointAuthenticator.swift new file mode 100644 index 000000000..194282795 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointAuthenticator.swift @@ -0,0 +1,42 @@ +// +// SharePointAuthenticator.swift +// Cryptomator +// +// Created by Tobias Hagemann on 11.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import GRDB +import Promises +import UIKit + +public class SharePointAuthenticator { + private static var coordinator: SharePointAuthenticationCoordinator? + + public static func authenticate(from viewController: UIViewController, cloudProviderAccountManager: CloudProviderAccountManager, microsoftGraphAccountManager: MicrosoftGraphAccountManager) -> Promise { + let navigationController = BaseNavigationController() + let sharePointCoordinator = SharePointAuthenticationCoordinator(navigationController: navigationController) + coordinator = sharePointCoordinator + viewController.present(navigationController, animated: true) + sharePointCoordinator.start() + return sharePointCoordinator.pendingAuthentication.then { credential -> CloudProviderAccount in + let newAccountUID = UUID().uuidString + let cloudProviderAccount = CloudProviderAccount(accountUID: newAccountUID, cloudProviderType: .microsoftGraph(type: .sharePoint)) + try cloudProviderAccountManager.saveNewAccount(cloudProviderAccount) // Make sure to save this first, because Microsoft Graph account has a reference to the Cloud Provider account. + do { + let microsoftGraphAccount = MicrosoftGraphAccount(accountUID: newAccountUID, credentialID: credential.credential.identifier, driveID: credential.driveID, siteURL: credential.siteURL, type: .sharePoint) + try microsoftGraphAccountManager.saveNewAccount(microsoftGraphAccount) + return cloudProviderAccount + } catch let dbError as DatabaseError where dbError.resultCode == .SQLITE_CONSTRAINT { + try cloudProviderAccountManager.removeAccount(with: newAccountUID) + let existingMicrosoftGraphAccount = try microsoftGraphAccountManager.getAccount(credentialID: credential.credential.identifier, driveID: credential.driveID, type: .sharePoint) + return try cloudProviderAccountManager.getAccount(for: existingMicrosoftGraphAccount.accountUID) + } catch { + throw error + } + }.always { + self.coordinator = nil + } + } +} diff --git a/Cryptomator/MicrosoftGraph/SharePointCredential.swift b/Cryptomator/MicrosoftGraph/SharePointCredential.swift new file mode 100644 index 000000000..1245d4db4 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointCredential.swift @@ -0,0 +1,16 @@ +// +// SharePointCredential.swift +// Cryptomator +// +// Created by Tobias Hagemann on 12.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import Foundation + +public struct SharePointCredential { + public let siteURL: URL + public let credential: MicrosoftGraphCredential + public let driveID: String +} diff --git a/Cryptomator/MicrosoftGraph/SharePointDriveListViewController.swift b/Cryptomator/MicrosoftGraph/SharePointDriveListViewController.swift new file mode 100644 index 000000000..072862a4c --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointDriveListViewController.swift @@ -0,0 +1,101 @@ +// +// SharePointDriveListViewController.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import UIKit + +class SharePointDriveListViewController: SingleSectionTableViewController { + weak var coordinator: (Coordinator & SharePointAuthenticating)? + private var viewModel: SharePointDriveListViewModel + + init(viewModel: SharePointDriveListViewModel) { + self.viewModel = viewModel + super.init() + } + + override func viewDidLoad() { + super.viewDidLoad() + title = LocalizedString.getValue("sharePoint.selectDrive.title") + tableView.register(TableViewCell.self, forCellReuseIdentifier: "SharePointDriveCell") + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) + navigationItem.leftBarButtonItem = cancelButton + // pull to refresh + initRefreshControl() + viewModel.startListenForChanges { [weak self] in + self?.refreshControl?.endRefreshing() + self?.tableView.reloadData() + } onError: { [weak self] error in + guard let self = self else { return } + self.coordinator?.handleError(error, for: self) + } + } + + private func initRefreshControl() { + refreshControl = UIRefreshControl() + refreshControl?.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged) + refreshControl?.beginRefreshing() + tableView.setContentOffset(CGPoint(x: 0, y: -(refreshControl?.frame.size.height ?? 0)), animated: true) + } + + @objc func pullToRefresh() { + viewModel.refreshItems() + } + + @objc func cancel() { + coordinator?.cancel() + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.drives.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) + let drive = viewModel.drives[indexPath.row] + cell.textLabel?.text = drive.name + cell.imageView?.image = UIImage(systemName: "folder") + return cell + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return LocalizedString.getValue("sharePoint.selectDrive.header.title") + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + let itemsLoading = refreshControl?.isRefreshing ?? true + if !itemsLoading, viewModel.drives.isEmpty { + return LocalizedString.getValue("sharePoint.selectDrive.emptyList.footer") + } else { + return nil + } + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + // Prevents the header title from being displayed in uppercase + guard let headerView = view as? UITableViewHeaderFooterView else { + return + } + headerView.textLabel?.text = self.tableView(tableView, titleForHeaderInSection: section) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedDrive = viewModel.drives[indexPath.row] + do { + let credential = SharePointCredential(siteURL: viewModel.siteURL, credential: viewModel.credential, driveID: selectedDrive.identifier) + try coordinator?.authenticated(credential) + } catch { + coordinator?.handleError(error, for: self) + } + } +} diff --git a/Cryptomator/MicrosoftGraph/SharePointDriveListViewModel.swift b/Cryptomator/MicrosoftGraph/SharePointDriveListViewModel.swift new file mode 100644 index 000000000..8a5feb1ad --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointDriveListViewModel.swift @@ -0,0 +1,50 @@ +// +// SharePointDriveListViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +class SharePointDriveListViewModel { + let credential: MicrosoftGraphCredential + let siteURL: URL + + private let discovery: MicrosoftGraphDiscovery + private var changeListener: (() -> Void)? + private var errorListener: ((Error) -> Void)? + private(set) var drives: [MicrosoftGraphDrive] = [] + + init(credential: MicrosoftGraphCredential, siteURL: URL) { + self.credential = credential + self.discovery = MicrosoftGraphDiscovery(credential: credential) + self.siteURL = siteURL + } + + func startListenForChanges(onChange: @escaping () -> Void, onError: @escaping (Error) -> Void) { + changeListener = onChange + errorListener = onError + refreshItems() + } + + func refreshItems() { + discovery.fetchSharePointSite(for: siteURL).then { site in + self.fetchDrives(for: site.identifier) + }.catch { error in + self.errorListener?(error) + } + } + + private func fetchDrives(for siteIdentifier: String) { + discovery.fetchSharePointDrives(for: siteIdentifier).then { drives in + self.drives = drives + self.changeListener?() + }.catch { error in + self.errorListener?(error) + } + } +} diff --git a/Cryptomator/MicrosoftGraph/SharePointURLValidationError+Localization.swift b/Cryptomator/MicrosoftGraph/SharePointURLValidationError+Localization.swift new file mode 100644 index 000000000..230b79974 --- /dev/null +++ b/Cryptomator/MicrosoftGraph/SharePointURLValidationError+Localization.swift @@ -0,0 +1,22 @@ +// +// SharePointURLValidationError+Localization.swift +// Cryptomator +// +// Created by Tobias Hagemann on 13.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +extension SharePointURLValidationError: LocalizedError { + public var errorDescription: String? { + switch self { + case .emptyURL: + return LocalizedString.getValue("sharePoint.urlValidation.error.emptyURL") + case .invalidURL: + return LocalizedString.getValue("sharePoint.urlValidation.error.invalidURL") + } + } +} diff --git a/Cryptomator/S3/S3AuthenticationViewModel.swift b/Cryptomator/S3/S3AuthenticationViewModel.swift index 00b71f73a..930b2ad86 100644 --- a/Cryptomator/S3/S3AuthenticationViewModel.swift +++ b/Cryptomator/S3/S3AuthenticationViewModel.swift @@ -52,11 +52,11 @@ class S3AuthenticationViewModel: ObservableObject { func saveS3Credential() { guard !secretKey.isEmpty, !accessKey.isEmpty, !existingBucket.isEmpty, !endpoint.isEmpty, !region.isEmpty, !displayName.isEmpty else { - loginState = .error(S3AuthenticationViewModelError.emptyField) + loginState = .error(S3AuthenticationError.emptyField) return } guard let url = URL(string: endpoint) else { - loginState = .error(S3AuthenticationViewModelError.invalidEndpoint) + loginState = .error(S3AuthenticationError.invalidEndpoint) return } let credential = S3Credential(accessKey: accessKey, @@ -78,7 +78,7 @@ class S3AuthenticationViewModel: ObservableObject { let convertedError: Error switch error { case LocalizedCloudProviderError.unauthorized, CloudProviderError.unauthorized: - convertedError = S3AuthenticationViewModelError.invalidCredentials + convertedError = S3AuthenticationError.invalidCredentials default: convertedError = error } @@ -93,13 +93,13 @@ enum S3LoginState { case notLoggedIn } -enum S3AuthenticationViewModelError: Error { +enum S3AuthenticationError: Error { case emptyField case invalidEndpoint case invalidCredentials } -extension S3AuthenticationViewModelError: LocalizedError { +extension S3AuthenticationError: LocalizedError { var errorDescription: String? { switch self { case .emptyField: diff --git a/Cryptomator/Settings/SettingsCoordinator.swift b/Cryptomator/Settings/SettingsCoordinator.swift index 77094fb8d..0fb2c2c00 100644 --- a/Cryptomator/Settings/SettingsCoordinator.swift +++ b/Cryptomator/Settings/SettingsCoordinator.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation import StoreKit @@ -49,7 +50,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, .microsoftGraph(type: .oneDrive), .microsoftGraph(type: .sharePoint), .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "") let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("settings.cloudServices") chooseCloudVC.coordinator = self @@ -119,7 +120,12 @@ extension SettingsCoordinator: CloudChoosing { extension SettingsCoordinator: AccountListing, DefaultShowEditAccountBehavior { func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared) - _ = authenticator.authenticate(cloudProviderType, from: viewController) + authenticator.authenticate(cloudProviderType, from: viewController).catch { error in + guard case CocoaError.userCancelled = error else { + self.handleError(error, for: self.navigationController) + return + } + } } func selectedAccont(_ account: AccountInfo) throws {} diff --git a/Cryptomator/VaultDetail/MoveVault/MoveVaultCoordinator.swift b/Cryptomator/VaultDetail/MoveVault/MoveVaultCoordinator.swift index 46b7918ea..2a040e19d 100644 --- a/Cryptomator/VaultDetail/MoveVault/MoveVaultCoordinator.swift +++ b/Cryptomator/VaultDetail/MoveVault/MoveVaultCoordinator.swift @@ -31,7 +31,6 @@ class MoveVaultCoordinator: Coordinator { let provider: CloudProvider do { provider = try CloudProviderDBManager.shared.getProvider(with: vaultInfo.delegateAccountUID) - } catch { handleError(error, for: navigationController) return @@ -64,10 +63,11 @@ extension MoveVaultCoordinator: FolderChoosing { close() } - func showCreateNewFolder(parentPath: CloudPath) { + func showCreateNewFolder(parentPath: CloudPath, delegate: ChooseFolderViewModelProtocol?) { let modalNavigationController = BaseNavigationController() let child = AuthenticatedFolderCreationCoordinator(navigationController: modalNavigationController, provider: provider, parentPath: parentPath) child.parentCoordinator = self + child.delegate = delegate childCoordinators.append(child) navigationController.topViewController?.present(modalNavigationController, animated: true) child.start() diff --git a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift index 3fb3951eb..5586880a5 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: @@ -58,9 +59,12 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode return try? credential.getUsername() case .localFileSystem: return nil - case .oneDrive: - let credential = try? OneDriveCredential(with: vault.delegateAccountUID) - return try? credential?.getUsername() + case let .microsoftGraph(type): + guard let account = try? MicrosoftGraphAccountDBManager.shared.getAccount(for: vault.delegateAccountUID) else { + return nil + } + let credential = MicrosoftGraphCredential(identifier: account.credentialID, type: type) + return try? credential.getUsername() case .pCloud: guard let credential = try? PCloudCredential(userID: vault.delegateAccountUID) else { return nil diff --git a/Cryptomator/VaultDetail/VaultDetailUnlockCoordinator.swift b/Cryptomator/VaultDetail/VaultDetailUnlockCoordinator.swift index 773210602..446a31e3f 100644 --- a/Cryptomator/VaultDetail/VaultDetailUnlockCoordinator.swift +++ b/Cryptomator/VaultDetail/VaultDetailUnlockCoordinator.swift @@ -10,10 +10,6 @@ import CryptomatorCommonCore import Promises import UIKit -enum VaultDetailUnlockError: Error { - case userCanceled -} - class VaultDetailUnlockCoordinator: NSObject, Coordinator, VaultPasswordVerifying, UIAdaptivePresentationControllerDelegate { var childCoordinators = [Coordinator]() weak var parentCoordinator: Coordinator? @@ -48,7 +44,7 @@ class VaultDetailUnlockCoordinator: NSObject, Coordinator, VaultPasswordVerifyin } func cancel() { - pendingAuthentication.reject(VaultDetailUnlockError.userCanceled) + pendingAuthentication.reject(CocoaError(.userCancelled)) close() } @@ -56,6 +52,6 @@ class VaultDetailUnlockCoordinator: NSObject, Coordinator, VaultPasswordVerifyin func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // User has canceled the authentication by closing the modal via swipe - pendingAuthentication.reject(VaultDetailUnlockError.userCanceled) + pendingAuthentication.reject(CocoaError(.userCancelled)) } } diff --git a/Cryptomator/VaultDetail/VaultDetailViewController.swift b/Cryptomator/VaultDetail/VaultDetailViewController.swift index 56862169e..d957c8397 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewController.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewController.swift @@ -123,7 +123,7 @@ class VaultDetailViewController: BaseUITableViewController { private func showUnlockScreen(for vault: VaultInfo, biometryTypeName: String) { coordinator?.unlockVault(vault, biometryTypeName: biometryTypeName).recover { error -> Void in - guard case VaultDetailUnlockError.userCanceled = error else { + guard case CocoaError.userCancelled = error else { throw error } }.catch { [weak self] error in diff --git a/Cryptomator/WebDAV/WebDAVAuthenticationCoordinator.swift b/Cryptomator/WebDAV/WebDAVAuthenticationCoordinator.swift index 0ec06179a..7f77cce9c 100644 --- a/Cryptomator/WebDAV/WebDAVAuthenticationCoordinator.swift +++ b/Cryptomator/WebDAV/WebDAVAuthenticationCoordinator.swift @@ -45,7 +45,7 @@ class WebDAVAuthenticationCoordinator: NSObject, Coordinator, WebDAVAuthenticati } func cancel() { - pendingAuthentication.reject(WebDAVAuthenticationError.userCanceled) + pendingAuthentication.reject(CocoaError(.userCancelled)) close() } @@ -53,6 +53,6 @@ class WebDAVAuthenticationCoordinator: NSObject, Coordinator, WebDAVAuthenticati func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // User has canceled the authentication by closing the modal via swipe - pendingAuthentication.reject(WebDAVAuthenticationError.userCanceled) + pendingAuthentication.reject(CocoaError(.userCancelled)) } } diff --git a/Cryptomator/WebDAV/WebDAVAuthenticationViewModel.swift b/Cryptomator/WebDAV/WebDAVAuthenticationViewModel.swift index 8c7ef66a6..58461c7b6 100644 --- a/Cryptomator/WebDAV/WebDAVAuthenticationViewModel.swift +++ b/Cryptomator/WebDAV/WebDAVAuthenticationViewModel.swift @@ -15,7 +15,6 @@ import Promises enum WebDAVAuthenticationError: Error { case invalidInput case untrustedCertificate(certificate: TLSCertificate, url: URL) - case userCanceled case httpConnection } diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 26987fb3d..f10907e98 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -26,7 +26,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.12.0")), + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "2.0.0")), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), .package(url: "https://github.com/PhilLibs/simple-swift-dependencies", .upToNextMajor(from: "0.1.0")), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")), diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index 1a0a9b773..b5257771d 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import CryptomatorCloudAccessCore import Dependencies import Foundation import GRDB @@ -91,6 +92,9 @@ public class CryptomatorDatabase { migrator.registerMigration("initialHubSupport") { db in try initialHubSupportMigration(db) } + migrator.registerMigration("microsoftGraphAccountMigration") { db in + try microsoftGraphAccountMigration(db) + } return migrator } @@ -206,6 +210,34 @@ public class CryptomatorDatabase { }) } + class func microsoftGraphAccountMigration(_ db: Database) throws { + try db.create(table: "microsoftGraphAccounts") { table in + table.column("accountUID", .text).primaryKey().references("cloudProviderAccounts", onDelete: .cascade) + table.column("credentialID", .text).notNull() + table.column("driveID", .text) + table.column("siteURL", .text) + table.column("type", .text).notNull() + table.uniqueKey(["credentialID", "driveID", "type"]) + } + try db.execute(sql: """ + CREATE UNIQUE INDEX uq_microsoftGraphAccounts_nullDriveID + ON microsoftGraphAccounts (credentialID, type) + WHERE driveID IS NULL + """) + enum LegacyCloudProviderType: Codable, DatabaseValueConvertible { case oneDrive } + let rows = try Row.fetchAll(db, sql: "SELECT accountUID FROM cloudProviderAccounts WHERE cloudProviderType = ?", arguments: [LegacyCloudProviderType.oneDrive.databaseValue]) + for row in rows { + let oldAccountUID: String = row["accountUID"] // which is the `credentialID` + let newAccountUID = UUID().uuidString + let newCloudProviderType = CloudProviderType.microsoftGraph(type: .oneDrive).databaseValue + try db.execute(sql: "UPDATE cloudProviderAccounts SET accountUID = ?, cloudProviderType = ? WHERE accountUID = ?", arguments: [newAccountUID, newCloudProviderType, oldAccountUID]) + try db.execute(sql: "UPDATE vaultAccounts SET delegateAccountUID = ? WHERE delegateAccountUID = ?", arguments: [newAccountUID, oldAccountUID]) + try db.execute(sql: "UPDATE accountListPosition SET accountUID = ?, cloudProviderType = ? WHERE accountUID = ?", arguments: [newAccountUID, newCloudProviderType, oldAccountUID]) + let newMicrosoftGraphType = MicrosoftGraphType.oneDrive.databaseValue + try db.execute(sql: "INSERT INTO microsoftGraphAccounts (accountUID, credentialID, driveID, siteURL, type) VALUES (?, ?, NULL, NULL, ?)", arguments: [newAccountUID, oldAccountUID, newMicrosoftGraphType]) + } + } + public static func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? 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/Manager/CloudProviderAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift index 590595e97..4fcfac029 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift @@ -35,7 +35,7 @@ public enum CloudProviderAccountError: Error { } public protocol CloudProviderAccountManager { - func getCloudProviderType(for accountUID: String) throws -> CloudProviderType + func getAccount(for accountUID: String) throws -> CloudProviderAccount func getAllAccountUIDs(for type: CloudProviderType) throws -> [String] func saveNewAccount(_ account: CloudProviderAccount) throws func removeAccount(with accountUID: String) throws @@ -45,14 +45,14 @@ public class CloudProviderAccountDBManager: CloudProviderAccountManager { @Dependency(\.database) var database public static let shared = CloudProviderAccountDBManager() - public func getCloudProviderType(for accountUID: String) throws -> CloudProviderType { - let cloudAccount = try database.read { db in + public func getAccount(for accountUID: String) throws -> CloudProviderAccount { + let account = try database.read { db in return try CloudProviderAccount.fetchOne(db, key: accountUID) } - guard let providerType = cloudAccount?.cloudProviderType else { + guard let account = account else { throw CloudProviderAccountError.accountNotFoundError } - return providerType + return account } public func getAllAccountUIDs(for type: CloudProviderType) throws -> [String] { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 5a3d4fadf..a8bdebfb9 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -59,9 +59,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating Creates and returns a cloud provider for the given `accountUID`. */ func createProvider(for accountUID: String) throws -> CloudProvider { - let cloudProviderType = try accountManager.getCloudProviderType(for: accountUID) + let account = try accountManager.getAccount(for: accountUID) let provider: CloudProvider - switch cloudProviderType { + switch account.cloudProviderType { case .box: let tokenStorage = BoxTokenStorage(userID: accountUID) let credential = BoxCredential(tokenStorage: tokenStorage) @@ -77,9 +77,10 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating throw CloudProviderAccountError.accountNotFoundError } provider = try LocalFileSystemProvider(rootURL: rootURL, maxPageSize: .max) - case .oneDrive: - let credential = try OneDriveCredential(with: accountUID) - provider = try OneDriveCloudProvider(credential: credential, maxPageSize: .max) + case let .microsoftGraph(type): + let account = try MicrosoftGraphAccountDBManager.shared.getAccount(for: accountUID) + let credential = MicrosoftGraphCredential(identifier: account.credentialID, type: type) + provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: account.driveID, maxPageSize: .max) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createClient(with: credential.user) @@ -109,10 +110,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating This is necessary because otherwise memory limit problems can occur with folders with many items in the `FileProviderExtension` where a background `URLSession` is used. */ func createBackgroundSessionProvider(for accountUID: String, sessionIdentifier: String) throws -> CloudProvider { - let cloudProviderType = try accountManager.getCloudProviderType(for: accountUID) + let account = try accountManager.getAccount(for: accountUID) let provider: CloudProvider - - switch cloudProviderType { + switch account.cloudProviderType { case .box: let tokenStorage = BoxTokenStorage(userID: accountUID) let credential = BoxCredential(tokenStorage: tokenStorage) @@ -128,9 +128,10 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating throw CloudProviderAccountError.accountNotFoundError } 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) + case let .microsoftGraph(type): + let account = try MicrosoftGraphAccountDBManager.shared.getAccount(for: accountUID) + let credential = MicrosoftGraphCredential(identifier: account.credentialID, type: type) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, driveIdentifier: account.driveID, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createBackgroundClient(with: credential.user, sessionIdentifier: sessionIdentifier) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift index 7ebdb1a5c..2ac25832c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift @@ -6,37 +6,21 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation import GRDB -public enum CloudProviderType: Codable, Equatable, Hashable { +public enum CloudProviderType: Codable, Equatable, Hashable, DatabaseValueConvertible { case box case dropbox case googleDrive case localFileSystem(type: LocalFileSystemType) - case oneDrive + case microsoftGraph(type: MicrosoftGraphType) case pCloud case s3(type: S3Type) case webDAV(type: WebDAVType) } -extension CloudProviderType: DatabaseValueConvertible { - public var databaseValue: DatabaseValue { - let jsonEncoder = JSONEncoder() - guard let data = try? jsonEncoder.encode(self), let string = String(data: data, encoding: .utf8) else { - return .null - } - return string.databaseValue - } - - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { - guard let string = String.fromDatabaseValue(dbValue) else { return nil } - let data = Data(string.utf8) - let jsonDecoder = JSONDecoder() - return try? jsonDecoder.decode(CloudProviderType.self, from: data) - } -} - public enum LocalFileSystemType: Codable { case custom case iCloudDrive diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphAccountDBManager.swift new file mode 100644 index 000000000..8b3c2a852 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphAccountDBManager.swift @@ -0,0 +1,96 @@ +// +// MicrosoftGraphAccountDBManager.swift +// CryptomatorCommonCore +// +// Created by Tobias Hagemann on 10.03.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import Dependencies +import Foundation +import GRDB + +public struct MicrosoftGraphAccount: Codable, FetchableRecord, TableRecord, Equatable { + public static let databaseTableName = "microsoftGraphAccounts" + static let accountUIDKey = "accountUID" + static let credentialIDKey = "credentialID" + static let driveIDKey = "driveID" + static let siteURLKey = "siteURL" + static let typeKey = "type" + + public let accountUID: String + public let credentialID: String + public let driveID: String? + public let siteURL: URL? + public let type: MicrosoftGraphType + + public init(accountUID: String, credentialID: String, driveID: String? = nil, siteURL: URL? = nil, type: MicrosoftGraphType) { + self.accountUID = accountUID + self.credentialID = credentialID + self.driveID = driveID + self.siteURL = siteURL + self.type = type + } +} + +extension MicrosoftGraphAccount: PersistableRecord { + public func encode(to container: inout PersistenceContainer) { + container[MicrosoftGraphAccount.accountUIDKey] = accountUID + container[MicrosoftGraphAccount.credentialIDKey] = credentialID + container[MicrosoftGraphAccount.driveIDKey] = driveID + container[MicrosoftGraphAccount.siteURLKey] = siteURL + container[MicrosoftGraphAccount.typeKey] = type + } +} + +public enum MicrosoftGraphAccountError: Error { + case accountNotFoundError +} + +public protocol MicrosoftGraphAccountManager { + func getAccount(for accountUID: String) throws -> MicrosoftGraphAccount + func getAccount(credentialID: String, driveID: String?, type: MicrosoftGraphType) throws -> MicrosoftGraphAccount + func multipleAccountsExist(for credentialID: String) throws -> Bool + func saveNewAccount(_ account: MicrosoftGraphAccount) throws +} + +public class MicrosoftGraphAccountDBManager: MicrosoftGraphAccountManager { + @Dependency(\.database) var database + public static let shared = MicrosoftGraphAccountDBManager() + + public func getAccount(for accountUID: String) throws -> MicrosoftGraphAccount { + let account = try database.read { db in + try MicrosoftGraphAccount.fetchOne(db, key: accountUID) + } + guard let account = account else { + throw MicrosoftGraphAccountError.accountNotFoundError + } + return account + } + + public func getAccount(credentialID: String, driveID: String?, type: MicrosoftGraphType) throws -> MicrosoftGraphAccount { + let account = try database.read { db in + return try MicrosoftGraphAccount + .filter(Column(MicrosoftGraphAccount.credentialIDKey) == credentialID && Column(MicrosoftGraphAccount.driveIDKey) == driveID && Column(MicrosoftGraphAccount.typeKey) == type.databaseValue) + .fetchOne(db) + } + guard let account = account else { + throw MicrosoftGraphAccountError.accountNotFoundError + } + return account + } + + public func multipleAccountsExist(for credentialID: String) throws -> Bool { + let count = try database.read { db in + try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM \(MicrosoftGraphAccount.databaseTableName) WHERE \(MicrosoftGraphAccount.credentialIDKey) = ?", arguments: [credentialID]) ?? 0 + } + return count > 1 + } + + public func saveNewAccount(_ account: MicrosoftGraphAccount) throws { + try database.write { db in + try account.save(db) + } + } +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift index ce7b48842..ceed66da6 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift @@ -23,18 +23,18 @@ class CloudProviderAccountManagerTests: XCTestCase { let accountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: accountUID, cloudProviderType: .googleDrive) try accountManager.saveNewAccount(account) - let fetchedCloudProviderType = try accountManager.getCloudProviderType(for: accountUID) - XCTAssertEqual(CloudProviderType.googleDrive, fetchedCloudProviderType) + let fetchedAccount = try accountManager.getAccount(for: accountUID) + XCTAssertEqual(CloudProviderType.googleDrive, fetchedAccount.cloudProviderType) } func testRemoveAccount() throws { let accountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: accountUID, cloudProviderType: .googleDrive) try accountManager.saveNewAccount(account) - let fetchedCloudProviderType = try accountManager.getCloudProviderType(for: accountUID) - XCTAssertEqual(CloudProviderType.googleDrive, fetchedCloudProviderType) + let fetchedAccount = try accountManager.getAccount(for: accountUID) + XCTAssertEqual(CloudProviderType.googleDrive, fetchedAccount.cloudProviderType) try accountManager.removeAccount(with: accountUID) - XCTAssertThrowsError(try accountManager.getCloudProviderType(for: accountUID)) { error in + XCTAssertThrowsError(try accountManager.getAccount(for: accountUID)) { error in guard case CloudProviderAccountError.accountNotFoundError = error else { XCTFail("Throws the wrong error: \(error)") return diff --git a/CryptomatorIntents/Common/VaultOptionsProvider.swift b/CryptomatorIntents/Common/VaultOptionsProvider.swift index fa4121093..60b6694c4 100644 --- a/CryptomatorIntents/Common/VaultOptionsProvider.swift +++ b/CryptomatorIntents/Common/VaultOptionsProvider.swift @@ -31,11 +31,11 @@ struct VaultOptionsProvider { func provideVaultOptionsCollection() async throws -> INObjectCollection { let vaultAccounts = try vaultAccountManager.getAllAccounts() let vaults: [Vault] = try vaultAccounts.map { - let cloudProviderType = try cloudProviderAccountManager.getCloudProviderType(for: $0.delegateAccountUID) + let cloudProviderAccount = try cloudProviderAccountManager.getAccount(for: $0.delegateAccountUID) return Vault(identifier: $0.vaultUID, display: $0.vaultName, subtitle: $0.vaultPath.path, - image: .init(type: cloudProviderType)) + image: .init(type: cloudProviderAccount.cloudProviderType)) } return INObjectCollection(items: vaults) } diff --git a/CryptomatorIntents/SaveFileIntentHandler.swift b/CryptomatorIntents/SaveFileIntentHandler.swift index d2a8dc37d..1aba760be 100644 --- a/CryptomatorIntents/SaveFileIntentHandler.swift +++ b/CryptomatorIntents/SaveFileIntentHandler.swift @@ -131,8 +131,8 @@ extension CloudProviderType { return "google-drive-vault" case let .localFileSystem(type): return type.assetName - case .oneDrive: - return "onedrive-vault" + case let .microsoftGraph(type): + return type.assetName case .pCloud: return "pcloud-vault" case .s3: @@ -154,6 +154,17 @@ extension LocalFileSystemType { } } +extension MicrosoftGraphType { + var assetName: String { + switch self { + case .oneDrive: + return "onedrive-vault" + case .sharePoint: + return "sharepoint-vault" + } + } +} + extension INImage { convenience init(type: CloudProviderType) { self.init(named: type.assetName) diff --git a/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift b/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift index 2fee61e20..471b4ad59 100644 --- a/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift +++ b/CryptomatorTests/AddLocalVault/AddLocalVaultViewModelTestCase.swift @@ -56,8 +56,7 @@ class AddLocalVaultViewModelTestCase: XCTestCase { } class CloudProviderAccountManagerMock: CloudProviderAccountManager { - var savedAccounts = [CloudProviderAccount]() - func getCloudProviderType(for accountUID: String) throws -> CloudProviderType { + func getAccount(for accountUID: String) throws -> CryptomatorCommonCore.CloudProviderAccount { throw MockError.notMocked } @@ -65,6 +64,8 @@ class AddLocalVaultViewModelTestCase: XCTestCase { throw MockError.notMocked } + var savedAccounts = [CloudProviderAccount]() + func saveNewAccount(_ account: CloudProviderAccount) throws { savedAccounts.append(account) } diff --git a/CryptomatorTests/S3AuthenticationViewModelTests.swift b/CryptomatorTests/S3AuthenticationViewModelTests.swift index 8e529122a..9c1b106df 100644 --- a/CryptomatorTests/S3AuthenticationViewModelTests.swift +++ b/CryptomatorTests/S3AuthenticationViewModelTests.swift @@ -105,7 +105,7 @@ class S3AuthenticationViewModelTests: XCTestCase { wait(for: recorder, timeout: 1.0) let stateChanges = recorder.getElements() - let expectedStateChanges: [S3LoginState] = [.notLoggedIn, .verifyingCredentials, .error(S3AuthenticationViewModelError.invalidCredentials)] + let expectedStateChanges: [S3LoginState] = [.notLoggedIn, .verifyingCredentials, .error(S3AuthenticationError.invalidCredentials)] XCTAssertEqual(expectedStateChanges, stateChanges) } @@ -120,7 +120,7 @@ class S3AuthenticationViewModelTests: XCTestCase { wait(for: recorder, timeout: 1.0) let stateChanges = recorder.getElements() - let expectedStateChanges: [S3LoginState] = [.notLoggedIn, .error(S3AuthenticationViewModelError.invalidEndpoint)] + let expectedStateChanges: [S3LoginState] = [.notLoggedIn, .error(S3AuthenticationError.invalidEndpoint)] XCTAssertEqual(expectedStateChanges, stateChanges) } 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 ccdb4bf79..a796380e2 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -65,10 +65,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 c121de218..721588996 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 eaee2c62b..6cc1e2e83 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -154,6 +154,8 @@ "maintenanceModeError.runningCloudTask" = "Operation cannot be performed because other background operations for this vault have to finish first. Please try again later."; +"microsoftGraphAuthenticator.error.serverDeclinedScopes" = "The necessary permissions to access the resources couldn't be obtained. Please try using a different account."; + "nameValidation.error.endsWithPeriod" = "You can't use a name that ends with a period. Please choose another name."; "nameValidation.error.endsWithSpace" = "You can't use a name that ends with a space. Please choose another name."; "nameValidation.error.containsIllegalCharacter" = "You can't use a name that contains \"%@\". Please choose another name."; @@ -211,6 +213,15 @@ "settings.shortcutsGuide" = "Shortcuts Guide"; "settings.unlockFullVersion" = "Unlock Full Version"; +"sharePoint.enterURL.title" = "Enter SharePoint URL"; +"sharePoint.enterURL.placeholder" = "SharePoint Site URL"; +"sharePoint.enterURL.header.title" = "Use this URL format:\nhttps://{…}.sharepoint.com/{sites|teams}/{…}"; +"sharePoint.urlValidation.error.emptyURL" = "The URL cannot be empty."; +"sharePoint.urlValidation.error.invalidURL" = "The URL is invalid. Please use this format: https://{…}.sharepoint.com/{sites|teams}/{…}"; +"sharePoint.selectDrive.title" = "Select Drive"; +"sharePoint.selectDrive.header.title" = "Select the SharePoint drive you want to work with."; +"sharePoint.selectDrive.emptyList.footer" = "No Drives Available"; + "snapshots.fileprovider.file1" = "/Accounting.numbers"; "snapshots.fileprovider.file2" = "/Final Presentation.key"; "snapshots.fileprovider.file3" = "/Product Trailer.mov"; 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