From fb7d06dcdcbd1087dfd9c3ebbfa498ebcf2aec70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Nov 2023 14:23:56 +0100 Subject: [PATCH 01/27] fix: Make sure iOS CI runs in iOS Simulator --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e836239..d53e47e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,9 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Build - run: swift build + run: xcodebuild -scheme InfomaniakCore build -destination "platform=iOS Simulator,name=iPhone 13,OS=latest" - name: Test - run: swift test + run: xcodebuild -scheme InfomaniakCore test -destination "platform=iOS Simulator,name=iPhone 13,OS=latest" build_and_test_macOS: name: Build and Test project on macOS From 63d7d27c982b3cf267ccde1225e31fc1d6506b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Nov 2023 17:11:50 +0100 Subject: [PATCH 02/27] fix: Rollback breaking changes from Mail to Drive --- .../Account/UserDefaults+Extension.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift index d646228..edc9fb2 100644 --- a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift +++ b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift @@ -26,6 +26,8 @@ public extension UserDefaults { self.rawValue = rawValue } + // TODO: Clean hotfix + static let legacyIsFirstLaunch = Keys(rawValue: "isFirstLaunch") static let currentUserId = Keys(rawValue: "currentUserId") } @@ -41,6 +43,16 @@ public extension UserDefaults { set(newValue, forKey: key(.currentUserId)) } } + + // TODO: Clean hotfix + var legacyIsFirstLaunch: Int { + get { + return integer(forKey: key(.currentUserId)) + } + set { + set(newValue, forKey: key(.currentUserId)) + } + } } // MARK: - Internal extension From 3a7b44be281dda176641a9a859a3d37115503f98 Mon Sep 17 00:00:00 2001 From: adrien-coye <121806582+adrien-coye@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:27:52 +0100 Subject: [PATCH 03/27] Revert "fix: Rollback breaking changes from Mail to Drive" --- .../Account/UserDefaults+Extension.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift index edc9fb2..d646228 100644 --- a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift +++ b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift @@ -26,8 +26,6 @@ public extension UserDefaults { self.rawValue = rawValue } - // TODO: Clean hotfix - static let legacyIsFirstLaunch = Keys(rawValue: "isFirstLaunch") static let currentUserId = Keys(rawValue: "currentUserId") } @@ -43,16 +41,6 @@ public extension UserDefaults { set(newValue, forKey: key(.currentUserId)) } } - - // TODO: Clean hotfix - var legacyIsFirstLaunch: Int { - get { - return integer(forKey: key(.currentUserId)) - } - set { - set(newValue, forKey: key(.currentUserId)) - } - } } // MARK: - Internal extension From ce1def613ac73127a563c80e421aa80c0bd161a8 Mon Sep 17 00:00:00 2001 From: adrien-coye <121806582+adrien-coye@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:28:20 +0100 Subject: [PATCH 04/27] Revert "Revert "fix: Rollback breaking changes from Mail to Drive"" --- .../Account/UserDefaults+Extension.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift index d646228..edc9fb2 100644 --- a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift +++ b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift @@ -26,6 +26,8 @@ public extension UserDefaults { self.rawValue = rawValue } + // TODO: Clean hotfix + static let legacyIsFirstLaunch = Keys(rawValue: "isFirstLaunch") static let currentUserId = Keys(rawValue: "currentUserId") } @@ -41,6 +43,16 @@ public extension UserDefaults { set(newValue, forKey: key(.currentUserId)) } } + + // TODO: Clean hotfix + var legacyIsFirstLaunch: Int { + get { + return integer(forKey: key(.currentUserId)) + } + set { + set(newValue, forKey: key(.currentUserId)) + } + } } // MARK: - Internal extension From 260584da8c7e3fe70f36d39ed9f1f8e520cc288c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Nov 2023 17:29:43 +0100 Subject: [PATCH 05/27] fix: New legacyIsFirstLaunch --- .../Account/UserDefaults+Extension.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift index edc9fb2..b0db01f 100644 --- a/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift +++ b/Sources/InfomaniakCore/Account/UserDefaults+Extension.swift @@ -45,12 +45,16 @@ public extension UserDefaults { } // TODO: Clean hotfix - var legacyIsFirstLaunch: Int { + var legacyIsFirstLaunch: Bool { get { - return integer(forKey: key(.currentUserId)) + if object(forKey: key(.legacyIsFirstLaunch)) != nil { + return bool(forKey: key(.legacyIsFirstLaunch)) + } else { + return true + } } set { - set(newValue, forKey: key(.currentUserId)) + set(newValue, forKey: key(.legacyIsFirstLaunch)) } } } From ad6b48e881f453aadc2572a6ff7ba72396663f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 28 Nov 2023 10:00:58 +0100 Subject: [PATCH 06/27] feat: SonarCloud conf file .sonarcloud.properties --- .sonarcloud.properties | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..92742db --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,15 @@ +## Path to sources +sonar.sources=Sources +# sonar.exclusions= +# sonar.inclusions= + +## Path to tests +sonar.tests=Tests +# sonar.test.exclusions= +# sonar.test.inclusions= + +## Source encoding +# sonar.sourceEncoding= + +# Exclusions for copy-paste detection +# sonar.cpd.exclusions= From 5155f39e26c2a9a81f724d06e95259442e756b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 29 Nov 2023 13:54:58 +0100 Subject: [PATCH 07/27] feat: Deprecate ParallelTaskMapper --- Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift b/Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift index f550655..a7b2047 100644 --- a/Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift +++ b/Sources/InfomaniakCore/Asynchronous/ParallelTaskMapper.swift @@ -29,6 +29,7 @@ public typealias SequenceableCollection = Sequence & Collection /// Use default settings for optimised queue depth /// @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +@available(*, deprecated, message: "Use .concurrentMap from the InfomaniakConcurrency package instead") public struct ParallelTaskMapper { /// private processing TaskQueue private let taskQueue: TaskQueue From 4b98a079c9f2aceadf7137a8755df73fcd96408d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 30 Nov 2023 17:28:03 +0100 Subject: [PATCH 08/27] docs: Deprecated chunked(into:) --- Sources/InfomaniakCore/Extensions/Array+Chunks.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/InfomaniakCore/Extensions/Array+Chunks.swift b/Sources/InfomaniakCore/Extensions/Array+Chunks.swift index 64d77ca..ffe9e2f 100644 --- a/Sources/InfomaniakCore/Extensions/Array+Chunks.swift +++ b/Sources/InfomaniakCore/Extensions/Array+Chunks.swift @@ -22,6 +22,7 @@ public extension Array { /// Subdivide an array into smaller parts /// - Parameter size: the size of the sub-division /// - Returns: The chunked structure + @available(*, deprecated, message: "Use chunks(ofCount: Int) form apple/swift-algorithms instead") func chunked(into size: Int) -> [[Element]] { return stride(from: 0, to: count, by: size).map { Array(self[$0 ..< Swift.min($0 + size, count)]) From ef6ba1a47b4b797d2db1abae91ddab9e3b4e9908 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 6 Dec 2023 14:25:01 +0100 Subject: [PATCH 09/27] fix: Handle 401 error at Authenticator level --- Sources/InfomaniakCore/Networking/ApiFetcher.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 4e418c2..6986538 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -29,7 +29,14 @@ public protocol RefreshTokenDelegate: AnyObject { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) open class ApiFetcher { public typealias RequestModifier = (inout URLRequest) throws -> Void - + + /// All status except 401 are handled by our code, 401 status is handled by Alamofire's Authenticator code + private static var handledHttpStatus: Set = { + var allStatus = Set(200 ... 500) + allStatus.remove(401) + return allStatus + }() + public var authenticatedSession: Session! public static var decoder: JSONDecoder = { let decoder = JSONDecoder() @@ -130,7 +137,8 @@ open class ApiFetcher { open func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) { - let response = await request.serializingDecodable(ApiResponse.self, + let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus) + let response = await validatedRequest.serializingDecodable(ApiResponse.self, automaticallyCancelling: true, decoder: decoder).response let apiResponse = try response.result.get() From e5b1c0f00a3b1dc7ebe012ebb0933cdf473cb6d4 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Tue, 2 Jan 2024 11:49:22 +0100 Subject: [PATCH 10/27] feat: Add DeeplinkService --- .../DeeplinkService/DeeplinkService.swift | 37 +++++++++++++++++++ .../GroupContainerService.swift | 33 +++++++++++++++++ .../DeeplinkService/URLOpenable.swift | 14 +++++++ 3 files changed, 84 insertions(+) create mode 100644 Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift create mode 100644 Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift create mode 100644 Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift diff --git a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift new file mode 100644 index 0000000..29711d8 --- /dev/null +++ b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift @@ -0,0 +1,37 @@ +// +// File.swift +// +// +// Created by Ambroise Decouttere on 28/12/2023. +// + +import Foundation +import InfomaniakDI +import Sentry + +@available(iOS 14, *) +public struct DeeplinkService { + @LazyInjectService private var urlOpener: URLOpenable + + private let group = "group.com.infomaniak" + private let kdriveAppStore = "https://itunes.apple.com/app/id1482778676" + + public init() {} + + public func shareFileToKdrive(_ url: URL) throws { + do { + guard let destination = try GroupContainerService.writeToGroupContainer(group: group, file: url) else { return } + + var targetUrl = URLComponents(string: "kdrive-file-sharing://file") + targetUrl?.queryItems = [URLQueryItem(name: "url", value: destination.path)] + if let targetAppUrl = targetUrl?.url, urlOpener.canOpen(url: targetAppUrl) { + urlOpener.openUrl(targetAppUrl) + } else { + urlOpener.openUrl(URL(string: "https://itunes.apple.com/app/id1482778676")!) + } + } catch { + SentrySDK.capture(error: error) + throw error + } + } +} diff --git a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift new file mode 100644 index 0000000..2a50fb8 --- /dev/null +++ b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift @@ -0,0 +1,33 @@ +// +// File.swift +// +// +// Created by Ambroise Decouttere on 28/12/2023. +// + +import Foundation + +@available(iOS 14, *) +struct GroupContainerService { + static func writeToGroupContainer(group: String, file: URL) throws -> URL? { + guard let sharedContainerURL: URL = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: group) else { return nil } + + let groupContainer = sharedContainerURL.appendingPathComponent("Library/Caches/file-sharing", conformingTo: .directory) + let destination = groupContainer.appendingPathComponent(file.lastPathComponent) + + do { + if FileManager.default.fileExists(atPath: groupContainer.path) { + try FileManager.default.removeItem(at: groupContainer) + } + try FileManager.default.createDirectory(at: groupContainer, withIntermediateDirectories: false) + try FileManager.default.copyItem( + at: file, + to: destination + ) + return destination + } catch { + throw error + } + } +} diff --git a/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift b/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift new file mode 100644 index 0000000..2cc2755 --- /dev/null +++ b/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Ambroise Decouttere on 28/12/2023. +// + +import Foundation + +public protocol URLOpenable { + func canOpen(url: URL) -> Bool + + func openUrl(_ url: URL) +} From af7f96d7f438cb6dbda15eec8d907e80c69ce542 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Thu, 4 Jan 2024 11:18:34 +0100 Subject: [PATCH 11/27] fix: Remove catch --- .../DeeplinkService/DeeplinkService.swift | 19 ++++++---------- .../GroupContainerService.swift | 22 ++++++++----------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift index 29711d8..7ead3ac 100644 --- a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift @@ -19,19 +19,14 @@ public struct DeeplinkService { public init() {} public func shareFileToKdrive(_ url: URL) throws { - do { - guard let destination = try GroupContainerService.writeToGroupContainer(group: group, file: url) else { return } + guard let destination = try GroupContainerService.writeToGroupContainer(group: group, file: url) else { return } - var targetUrl = URLComponents(string: "kdrive-file-sharing://file") - targetUrl?.queryItems = [URLQueryItem(name: "url", value: destination.path)] - if let targetAppUrl = targetUrl?.url, urlOpener.canOpen(url: targetAppUrl) { - urlOpener.openUrl(targetAppUrl) - } else { - urlOpener.openUrl(URL(string: "https://itunes.apple.com/app/id1482778676")!) - } - } catch { - SentrySDK.capture(error: error) - throw error + var targetUrl = URLComponents(string: "kdrive-file-sharing://file") + targetUrl?.queryItems = [URLQueryItem(name: "url", value: destination.path)] + if let targetAppUrl = targetUrl?.url, urlOpener.canOpen(url: targetAppUrl) { + urlOpener.openUrl(targetAppUrl) + } else { + urlOpener.openUrl(URL(string: "https://itunes.apple.com/app/id1482778676")!) } } } diff --git a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift index 2a50fb8..a0198d3 100644 --- a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Ambroise Decouttere on 28/12/2023. // @@ -16,18 +16,14 @@ struct GroupContainerService { let groupContainer = sharedContainerURL.appendingPathComponent("Library/Caches/file-sharing", conformingTo: .directory) let destination = groupContainer.appendingPathComponent(file.lastPathComponent) - do { - if FileManager.default.fileExists(atPath: groupContainer.path) { - try FileManager.default.removeItem(at: groupContainer) - } - try FileManager.default.createDirectory(at: groupContainer, withIntermediateDirectories: false) - try FileManager.default.copyItem( - at: file, - to: destination - ) - return destination - } catch { - throw error + if FileManager.default.fileExists(atPath: groupContainer.path) { + try FileManager.default.removeItem(at: groupContainer) } + try FileManager.default.createDirectory(at: groupContainer, withIntermediateDirectories: false) + try FileManager.default.copyItem( + at: file, + to: destination + ) + return destination } } From 1afed30fac5dc5e437ae2afe0fd4e46e52c7f572 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Thu, 4 Jan 2024 12:47:15 +0100 Subject: [PATCH 12/27] fix: Code smells --- .../DeeplinkService/DeeplinkService.swift | 25 +++++++++++++------ .../GroupContainerService.swift | 23 ++++++++++++----- .../DeeplinkService/URLOpenable.swift | 23 ++++++++++++----- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift index 7ead3ac..b6be5b3 100644 --- a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift @@ -1,9 +1,20 @@ -// -// File.swift -// -// -// Created by Ambroise Decouttere on 28/12/2023. -// +/* + Infomaniak Core - iOS + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ import Foundation import InfomaniakDI @@ -16,7 +27,7 @@ public struct DeeplinkService { private let group = "group.com.infomaniak" private let kdriveAppStore = "https://itunes.apple.com/app/id1482778676" - public init() {} + public init() { /*Empty on purpose*/ } public func shareFileToKdrive(_ url: URL) throws { guard let destination = try GroupContainerService.writeToGroupContainer(group: group, file: url) else { return } diff --git a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift index a0198d3..c1670cd 100644 --- a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift @@ -1,9 +1,20 @@ -// -// File.swift -// -// -// Created by Ambroise Decouttere on 28/12/2023. -// +/* + Infomaniak Core - iOS + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ import Foundation diff --git a/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift b/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift index 2cc2755..7991182 100644 --- a/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift +++ b/Sources/InfomaniakCore/DeeplinkService/URLOpenable.swift @@ -1,9 +1,20 @@ -// -// File.swift -// -// -// Created by Ambroise Decouttere on 28/12/2023. -// +/* + Infomaniak Core - iOS + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ import Foundation From 552e64609f967627334372af63e5381da6f78dde Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Thu, 4 Jan 2024 13:21:15 +0100 Subject: [PATCH 13/27] fix: Code smells --- Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift | 3 +-- .../InfomaniakCore/DeeplinkService/GroupContainerService.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift index b6be5b3..4fa6704 100644 --- a/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/DeeplinkService.swift @@ -20,14 +20,13 @@ import Foundation import InfomaniakDI import Sentry -@available(iOS 14, *) public struct DeeplinkService { @LazyInjectService private var urlOpener: URLOpenable private let group = "group.com.infomaniak" private let kdriveAppStore = "https://itunes.apple.com/app/id1482778676" - public init() { /*Empty on purpose*/ } + public init() { /* Empty on purpose */ } public func shareFileToKdrive(_ url: URL) throws { guard let destination = try GroupContainerService.writeToGroupContainer(group: group, file: url) else { return } diff --git a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift index c1670cd..20eb2b8 100644 --- a/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift +++ b/Sources/InfomaniakCore/DeeplinkService/GroupContainerService.swift @@ -18,13 +18,12 @@ import Foundation -@available(iOS 14, *) struct GroupContainerService { static func writeToGroupContainer(group: String, file: URL) throws -> URL? { guard let sharedContainerURL: URL = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: group) else { return nil } - let groupContainer = sharedContainerURL.appendingPathComponent("Library/Caches/file-sharing", conformingTo: .directory) + let groupContainer = sharedContainerURL.appendingPathComponent("Library/Caches/file-sharing", isDirectory: true) let destination = groupContainer.appendingPathComponent(file.lastPathComponent) if FileManager.default.fileExists(atPath: groupContainer.path) { From 9159bf5ae35a9d9105989c7666e8a400e39c4910 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Thu, 4 Jan 2024 13:37:47 +0100 Subject: [PATCH 14/27] fix: Mac CI --- Sources/InfomaniakCore/Hashing/StreamHasher.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/InfomaniakCore/Hashing/StreamHasher.swift b/Sources/InfomaniakCore/Hashing/StreamHasher.swift index 611f733..88770dd 100644 --- a/Sources/InfomaniakCore/Hashing/StreamHasher.swift +++ b/Sources/InfomaniakCore/Hashing/StreamHasher.swift @@ -46,9 +46,9 @@ public final class StreamHasher { private var isNotDone: Bool { switch state { case .begin, .progress: - true + return true case .done: - false + return false } } From 00462bd0394242779f60ae0b96bebe158a0ca2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 10 Jan 2024 16:17:01 +0100 Subject: [PATCH 15/27] feat: ItemProviderURLRepresentation can extract a file name test: Unit testing the title production --- .../ItemProviderURLRepresentation.swift | 25 +++++++----- .../UTItemProviderURLRepresentation.swift | 40 ++++++++++++------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderURLRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderURLRepresentation.swift index 35bb9eb..bced305 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderURLRepresentation.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderURLRepresentation.swift @@ -47,7 +47,7 @@ public final class ItemProviderURLRepresentation: NSObject, ProgressResultable { case localFileNotFound } - public typealias Success = URL + public typealias Success = (url: URL, title: String) public typealias Failure = Error public init(from itemProvider: NSItemProvider) throws { @@ -91,18 +91,23 @@ public final class ItemProviderURLRepresentation: NSObject, ProgressResultable { .deletingPathExtension .replacingOccurrences(of: "/", with: "") let fileName: String + let fileTitle: String if currentName.isEmpty { if #available(iOS 16.0, macOS 13.0, *), - let hostName = url.host()? - .replacingOccurrences(of: "www.", with: "") - .replacingOccurrences(of: ".", with: "_"), - !hostName.isEmpty { + let hostName = url.host()? + .replacingOccurrences(of: "www.", with: "") + .replacingOccurrences(of: ".", with: "_"), + !hostName.isEmpty { fileName = "\(hostName).webloc" + fileTitle = hostName } else { - fileName = "\(URL.defaultFileName()).webloc" + let defaultFileName = URL.defaultFileName() + fileName = "\(defaultFileName).webloc" + fileTitle = fileName } } else { fileName = "\(currentName).webloc" + fileTitle = currentName } let targetURL = try URL.temporaryUniqueFolderURL().appendingPathComponent(fileName) @@ -111,7 +116,7 @@ public final class ItemProviderURLRepresentation: NSObject, ProgressResultable { try data.write(to: targetURL) completionProgress.completedUnitCount += Self.progressStep - flowToAsync.sendSuccess(targetURL) + flowToAsync.sendSuccess((targetURL, fileTitle)) } /// Move a local file for later use @@ -133,7 +138,9 @@ public final class ItemProviderURLRepresentation: NSObject, ProgressResultable { try fileManager.copyItem(at: url, to: targetURL) completionProgress.completedUnitCount += Self.progressStep - flowToAsync.sendSuccess(targetURL) + + let fileNameWithoutExtension = (fileName as NSString).deletingPathExtension + flowToAsync.sendSuccess((targetURL, fileNameWithoutExtension)) return true } @@ -142,7 +149,7 @@ public final class ItemProviderURLRepresentation: NSObject, ProgressResultable { public var progress: Progress - public var result: Result { + public var result: Result { get async { return await flowToAsync.result } diff --git a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderURLRepresentation.swift b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderURLRepresentation.swift index 87acac8..61e036b 100644 --- a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderURLRepresentation.swift +++ b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderURLRepresentation.swift @@ -71,8 +71,9 @@ final class UTItemProviderURLRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.lastPathComponent, Self.imageFile + ".jpg") - XCTAssertTrue(success.lastPathComponent.hasSuffix(".jpg")) + XCTAssertEqual(success.url.lastPathComponent, Self.imageFile + ".jpg") + XCTAssertTrue(success.url.lastPathComponent.hasSuffix(".jpg")) + XCTAssertEqual(success.title, UTItemProviderURLRepresentation.imageFile) } catch { XCTFail("Unexpected \(error)") @@ -94,21 +95,23 @@ final class UTItemProviderURLRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertGreaterThanOrEqual(success.lastPathComponent.count, 17 + ".webloc".count, "non empty title") - XCTAssertLessThanOrEqual(success.lastPathComponent.count, 30 + ".webloc".count, "smaller than UUID") - XCTAssertTrue(success.lastPathComponent.hasSuffix(".webloc")) + XCTAssertGreaterThanOrEqual(success.url.lastPathComponent.count, 17 + ".webloc".count, "non empty title") + XCTAssertLessThanOrEqual(success.url.lastPathComponent.count, 30 + ".webloc".count, "smaller than UUID") + XCTAssertTrue(success.url.lastPathComponent.hasSuffix(".webloc")) + XCTAssertGreaterThanOrEqual(success.title.count, 10, "title should be non empty") } catch { XCTFail("Unexpected \(error)") } } - + @available(iOS 16.0, *) func testWebloc_successWebMainPage() async { // GIVEN let someURL = URL(string: "https://infomaniak.com/")! let item = NSItemProvider(contentsOf: someURL)! - let expectedFileName = "infomaniak_com.webloc" + let expectedTitle = "infomaniak_com" + let expectedFileName = "\(expectedTitle).webloc" do { let provider = try ItemProviderURLRepresentation(from: item) @@ -117,24 +120,26 @@ final class UTItemProviderURLRepresentation: XCTestCase { // WHEN let success = try await provider.result.get() - let title = success.lastPathComponent + let title = success.url.lastPathComponent // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") XCTAssertTrue(title.hasSuffix(".webloc"), "Expecting a .webloc extension, got:\(title)") XCTAssertEqual(title, expectedFileName) + XCTAssertEqual(success.title, expectedTitle) } catch { XCTFail("Unexpected \(error)") } } - + @available(iOS 16.0, *) func testWebloc_successWebMainPageWWW() async { // GIVEN let someURL = URL(string: "https://www.infomaniak.com/")! let item = NSItemProvider(contentsOf: someURL)! - let expectedFileName = "infomaniak_com.webloc" + let expectedTitle = "infomaniak_com" + let expectedFileName = "\(expectedTitle).webloc" do { let provider = try ItemProviderURLRepresentation(from: item) @@ -143,12 +148,13 @@ final class UTItemProviderURLRepresentation: XCTestCase { // WHEN let success = try await provider.result.get() - let title = success.lastPathComponent + let title = success.url.lastPathComponent // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") XCTAssertTrue(title.hasSuffix(".webloc"), "Expecting a .webloc extension, got:\(title)") XCTAssertEqual(title, expectedFileName) + XCTAssertEqual(success.title, expectedTitle) } catch { XCTFail("Unexpected \(error)") @@ -160,7 +166,8 @@ final class UTItemProviderURLRepresentation: XCTestCase { // GIVEN let someURL = URL(string: "https://infomaniak.com")! let item = NSItemProvider(contentsOf: someURL)! - let expectedFileName = "infomaniak_com.webloc" + let expectedTitle = "infomaniak_com" + let expectedFileName = "\(expectedTitle).webloc" do { let provider = try ItemProviderURLRepresentation(from: item) @@ -169,12 +176,13 @@ final class UTItemProviderURLRepresentation: XCTestCase { // WHEN let success = try await provider.result.get() - let title = success.lastPathComponent + let title = success.url.lastPathComponent // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") XCTAssertTrue(title.hasSuffix(".webloc"), "Expecting a .webloc extension, got:\(title)") XCTAssertEqual(title, expectedFileName) + XCTAssertEqual(success.title, expectedTitle) } catch { XCTFail("Unexpected \(error)") @@ -183,7 +191,8 @@ final class UTItemProviderURLRepresentation: XCTestCase { func testWebloc_successWebIndexPage() async { // GIVEN - let someURL = URL(string: "https://infomaniak.com/index.html")! + let expectedTitle = "index" + let someURL = URL(string: "https://infomaniak.com/\(expectedTitle).html")! let item = NSItemProvider(contentsOf: someURL)! do { @@ -196,7 +205,8 @@ final class UTItemProviderURLRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.lastPathComponent, "index.webloc") + XCTAssertEqual(success.url.lastPathComponent, "index.webloc") + XCTAssertEqual(success.title, expectedTitle) } catch { XCTFail("Unexpected \(error)") From 78a9d128fbd0ac8a8dcc5900fdc0bffee491d25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 10 Jan 2024 16:37:01 +0100 Subject: [PATCH 16/27] feat: UTItemProviderFileRepresentation can extract a file name test: Unit testing the title production --- .../ItemProviderFileRepresentation.swift | 9 +++-- .../ItemProviderTextRepresentation.swift | 2 +- .../UTItemProviderFileRepresentation.swift | 40 +++++++++++++------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift index a671d74..048346c 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift @@ -43,7 +43,7 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable case UnableToLoadFile } - public typealias Success = URL + public typealias Success = (url: URL, title: String) public typealias Failure = Error /// Init method @@ -55,7 +55,7 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable var typeIdentifiers = itemProvider.registeredTypeIdentifiers // make sure live photo identifier is at the end of supported formats - if let matchIndex = typeIdentifiers.index(of: Self.livePhotoIdentifier) { + if let matchIndex = typeIdentifiers.firstIndex(of: Self.livePhotoIdentifier) { typeIdentifiers.remove(at: matchIndex) typeIdentifiers.append(Self.livePhotoIdentifier) } @@ -91,12 +91,13 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable @InjectService var pathProvider: AppGroupPathProvidable let temporaryURL = try URL.temporaryUniqueFolderURL() + let title = (fileProviderURL.lastPathComponent as NSString).deletingPathExtension let fileName = fileProviderURL.appendingPathExtension(for: uti).lastPathComponent let temporaryFileURL = temporaryURL.appendingPathComponent(fileName) try fileManager.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path) completionProgress.completedUnitCount += Self.progressStep - flowToAsync.sendSuccess(temporaryFileURL) + flowToAsync.sendSuccess((temporaryFileURL, title)) } catch { completionProgress.completedUnitCount += Self.progressStep flowToAsync.sendFailure(error) @@ -109,7 +110,7 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable public var progress: Progress - public var result: Result { + public var result: Result { get async { await flowToAsync.result } diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift index 1e1f060..b833cc5 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift @@ -135,7 +135,7 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable public var progress: Progress - public var result: Result { + public var result: Result { get async { return await flowToAsync.result } diff --git a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderFileRepresentation.swift b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderFileRepresentation.swift index 1d039c5..8e41c26 100644 --- a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderFileRepresentation.swift +++ b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderFileRepresentation.swift @@ -51,6 +51,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testFile() async { // GIVEN + let expectedTitle = "text" let someText: NSString = "Some Text" // for NSCoding let item = NSItemProvider(item: someText, typeIdentifier: "\(UTI.text.rawValue)") @@ -78,9 +79,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "txt") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "txt") - let stringResult = try String(contentsOf: success, encoding: .utf8) as NSString // for NSCoding + let stringResult = try String(contentsOf: success.url, encoding: .utf8) as NSString // for NSCoding XCTAssertEqual(stringResult, someText) } catch { @@ -94,6 +96,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testJPGImageNoChange() async { // GIVEN + let expectedTitle = "JPEG image" guard let imgUrlJpg = Bundle.module.url(forResource: Self.imageFile, withExtension: "jpg") else { XCTFail("unexpected") return @@ -114,9 +117,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "jpeg") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "jpeg") - let imageData = try Data(contentsOf: success) + let imageData = try Data(contentsOf: success.url) XCTAssertTrue(imageData.count > 0, "expecting to find a non empty file") } catch { XCTFail("Unexpected \(error)") @@ -125,6 +129,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testHEICImageNoChange() async { // GIVEN + let expectedTitle = "HEIF Image" guard let imgUrlHeic = Bundle.module.url(forResource: Self.imageFile, withExtension: "heic") else { XCTFail("unexpected") return @@ -145,9 +150,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "heic") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "heic") - let imageData = try Data(contentsOf: success) + let imageData = try Data(contentsOf: success.url) XCTAssertTrue(imageData.count > 0, "expecting to find a non empty file") } catch { XCTFail("Unexpected \(error)") @@ -158,6 +164,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testHEICtoJPEG_success() async { // GIVEN + let expectedTitle = "JPEG image" guard let imgUrlHeic = Bundle.module.url(forResource: Self.imageFile, withExtension: "heic"), let imgUrlJpg = Bundle.module.url(forResource: Self.imageFile, withExtension: "jpg") else { XCTFail("unexpected") @@ -182,7 +189,8 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "jpeg") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "jpeg") } catch { XCTFail("Unexpected \(error)") } @@ -190,6 +198,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testJPEGtoHEIC_success() async { // GIVEN + let expectedTitle = "JPEG image" guard let imgUrlHeic = Bundle.module.url(forResource: Self.imageFile, withExtension: "heic"), let imgUrlJpg = Bundle.module.url(forResource: Self.imageFile, withExtension: "jpg") else { XCTFail("unexpected") @@ -214,7 +223,8 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "jpeg") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "jpeg") } catch { XCTFail("Unexpected \(error)") } @@ -224,6 +234,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testHEICtoJPG_fallbackToHEIC() async { // GIVEN + let expectedTitle = "HEIF Image" guard let imgUrlHeic = Bundle.module.url(forResource: Self.imageFile, withExtension: "heic") else { XCTFail("unexpected") return @@ -247,9 +258,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") + XCTAssertEqual(success.title, expectedTitle) // We expect a HEIC fallback when no JPG is available - XCTAssertEqual(success.pathExtension.lowercased(), "heic") + XCTAssertEqual(success.url.pathExtension.lowercased(), "heic") } catch { XCTFail("Unexpected \(error)") } @@ -257,6 +269,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testJPGtoHEIC_fallbackToJPEG() async { // GIVEN + let expectedTitle = "JPEG image" guard let imgUrlJpg = Bundle.module.url(forResource: Self.imageFile, withExtension: "jpg") else { XCTFail("unexpected") return @@ -280,9 +293,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") + XCTAssertEqual(success.title, expectedTitle) // We expect a JPEG fallback when no HEIC is available - XCTAssertEqual(success.pathExtension.lowercased(), "jpeg") + XCTAssertEqual(success.url.pathExtension.lowercased(), "jpeg") } catch { XCTFail("Unexpected \(error)") } @@ -292,6 +306,7 @@ final class UTItemProviderFileRepresentation: XCTestCase { func testJPGfromText_fallback() async { // GIVEN + let expectedTitle = "text" let someText: NSString = "Some Text" // for NSCoding let item = NSItemProvider(item: someText, typeIdentifier: "\(UTI.text.rawValue)") @@ -319,9 +334,10 @@ final class UTItemProviderFileRepresentation: XCTestCase { // THEN we still get an unchanged TXT file XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertEqual(success.pathExtension.lowercased(), "txt") + XCTAssertEqual(success.title, expectedTitle) + XCTAssertEqual(success.url.pathExtension.lowercased(), "txt") - let stringResult = try String(contentsOf: success, encoding: .utf8) as NSString // for NSCoding + let stringResult = try String(contentsOf: success.url, encoding: .utf8) as NSString // for NSCoding XCTAssertEqual(stringResult, someText) } catch { From be7ca1b9352ca5cf23d585d7e21786daf10ba784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 11 Jan 2024 09:27:32 +0100 Subject: [PATCH 17/27] feat: ItemProviderZipRepresentation can extract a file name test: Unit testing the title production --- .../ItemProviderZipRepresentation.swift | 6 ++-- .../UTItemProviderZipRepresentation.swift | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift index 1a9ff41..30d0a93 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift @@ -44,7 +44,7 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable { case notADirectory } - public typealias Success = URL + public typealias Success = (url: URL, title: String) public typealias Failure = Error public init(from itemProvider: NSItemProvider) throws { @@ -87,7 +87,7 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable { try self.fileManager.copyItem(at: zipURL, to: targetURL) completionProgress.completedUnitCount += Self.progressStep - self.flowToAsync.sendSuccess(targetURL) + self.flowToAsync.sendSuccess((targetURL, fileName)) } catch { completionProgress.completedUnitCount += Self.progressStep self.flowToAsync.sendFailure(error) @@ -101,7 +101,7 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable { public var progress: Progress - public var result: Result { + public var result: Result { get async { return await flowToAsync.result } diff --git a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderZipRepresentation.swift b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderZipRepresentation.swift index 0320a1f..83f2066 100644 --- a/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderZipRepresentation.swift +++ b/Tests/InfomaniakCoreTests/UTItemProviderFileRepresentation/UTItemProviderZipRepresentation.swift @@ -58,6 +58,14 @@ final class UTItemProviderZipRepresentation: XCTestCase { // URL of a file somewhere we have access to let mockedFileURL = try await provider.result.get() + let pathComponents = mockedFileURL.pathComponents + let pathComponentsCount = pathComponents.count + let fileName = mockedFileURL.lastPathComponent + guard let folderName = mockedFileURL.pathComponents[safe: pathComponentsCount - 2] else { + XCTFail("Unexpected, should be able to get a folder name") + return + } + // Sanity check XCTAssertTrue(mockedFileURL.lastPathComponent.hasSuffix("txt")) let folderToZipURL = mockedFileURL.deletingLastPathComponent() @@ -72,29 +80,40 @@ final class UTItemProviderZipRepresentation: XCTestCase { XCTAssertFalse(progress.isFinished, "Expecting the progress to reflect that the task has not started yet") // WHEN We get the path of the zipped file - let successURL = try await zipFileRepresentation.result.get() + let success = try await zipFileRepresentation.result.get() // THEN XCTAssertTrue(progress.isFinished, "Expecting the progress to reflect that the task is finished") - XCTAssertGreaterThanOrEqual(successURL.lastPathComponent.count, 30+".zip".count, "it should reflect the folder used, a UUID here") - XCTAssertEqual(successURL.pathExtension, "zip") - + XCTAssertEqual(success.title, folderName, "Expecting the generated zip file to match the compressed folder name") + XCTAssertGreaterThanOrEqual( + success.url.lastPathComponent.count, + 30 + ".zip".count, + "it should reflect the folder used, a UUID here" + ) + XCTAssertEqual(success.url.pathExtension, "zip") + // We create a folder to unzip content - let unzipFolder = successURL.deletingLastPathComponent() + let unzipFolder = success.url.deletingLastPathComponent() .appendingPathComponent("Unzip", isDirectory: true) try fileManager.createDirectory(at: unzipFolder, withIntermediateDirectories: true) // We build the URL of the unzipped folder - let fileName = successURL.lastPathComponent - let unzipFolderName = fileName.replacingOccurrences(of: ".zip", with: "") + let fileNameFromUrl = success.url.lastPathComponent + let unzipFolderName = fileNameFromUrl.replacingOccurrences(of: ".zip", with: "") let unzipFolderPath = unzipFolder.appendingPathComponent(unzipFolderName, isDirectory: true) // We perform the unzip - try fileManager.unzipItem(at: successURL, to: unzipFolder) + try fileManager.unzipItem(at: success.url, to: unzipFolder) // check the content of the unzipped folder let items = try fileManager.contentsOfDirectory(at: unzipFolderPath, includingPropertiesForKeys: nil) XCTAssertEqual(items.count, 1) + guard let unzippedItem = items.first else { + XCTFail("Unexpected empty folder") + return + } + + XCTAssertEqual(unzippedItem.lastPathComponent, fileName, "file name should not change once unzipped") guard let unzipFileURL = items.first else { XCTFail("Expecting to find exactly one file in unzipped folder") From 6643696ed4c2c56e5ae10c8d0c130b7d9284078c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 11 Jan 2024 10:07:31 +0100 Subject: [PATCH 18/27] chore: Bump dependencies --- Package.resolved | 28 ++++++++++++++-------------- Package.swift | 12 ++++++------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Package.resolved b/Package.resolved index 565875b..b5e229c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire", "state" : { - "revision" : "bc268c28fb170f494de9e9927c371b8342979ece", - "version" : "5.7.1" + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "0188d31089b5881a269e01777be74c7316924346", - "version" : "3.8.0" + "revision" : "363ed23d19a931809ea834a7d722da830353806a", + "version" : "3.8.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-dependency-injection", "state" : { - "revision" : "41a99aeb652d5294355129bfa70d1ea8c17b9980", - "version" : "1.1.10" + "revision" : "8dc9e67e6d3d9f4f5bd02d693a7ce1f93b125bcd", + "version" : "2.0.1" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "f1434caadda443b4ed2261b91ea4f43ab1ee2aa5", - "version" : "13.15.1" + "revision" : "7227d6a447821c28895daa099b6c7cd4c99d461b", + "version" : "13.25.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-swift", "state" : { - "revision" : "b287dc102036ff425bd8a88483f0a5596871f05e", - "version" : "10.41.0" + "revision" : "836cc4b8619886f979f8961c3f592a82b0741591", + "version" : "10.45.3" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "e46936ed191c0112cd3276e1c10c0bb7f865268e", - "version" : "8.9.1" + "revision" : "3b9a8e69ca296bd8cd0e317ad7a448e5daf4a342", + "version" : "8.18.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation.git", "state" : { - "revision" : "43ec568034b3731101dbf7670765d671c30f54f3", - "version" : "0.9.16" + "revision" : "a3f5c2bae0f04b0bce9ef3c4ba6bd1031a0564c4", + "version" : "0.9.17" } } ], diff --git a/Package.swift b/Package.swift index a222199..aa7a522 100644 --- a/Package.swift +++ b/Package.swift @@ -16,12 +16,12 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "1.1.10")), - .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.2.2")), - .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.3.1")), - .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.0.0")), - .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.7.0")), - .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")) + .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.1")), + .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), + .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.3")), + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.8.2")), + .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.17")) ], targets: [ .target( From b9fd5c8d84c08dd8745ef5d811da46ed3d0c6068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 11 Jan 2024 10:48:40 +0100 Subject: [PATCH 19/27] chore: SemVer changes --- Package.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index aa7a522..5fd0dc6 100644 --- a/Package.swift +++ b/Package.swift @@ -16,12 +16,12 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.1")), - .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.1")), + .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")), .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), - .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.3")), - .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.8.2")), - .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.17")) + .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.0")), + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.8.0")), + .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")) ], targets: [ .target( From 03113a229f01d539dd580421e03dd19c48ffa5f5 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 9 Jan 2024 12:54:59 +0100 Subject: [PATCH 20/27] refactor: Remove applicationPassword getApiToken --- .../Requests/InfomaniakNetworkLogin.swift | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift index 31e77d6..2edd658 100644 --- a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift +++ b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift @@ -36,9 +36,6 @@ public protocol InfomaniakNetworkLoginable { /// Get an api token async (callback on background thread) func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) - /// Get an api token async from an application password (callback on background thread) - func getApiToken(username: String, applicationPassword: String, completion: @escaping (ApiToken?, Error?) -> Void) - /// Refresh api token async (callback on background thread) func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) @@ -81,23 +78,6 @@ public class InfomaniakNetworkLogin: InfomaniakNetworkLoginable { getApiToken(request: request, completion: completion) } - public func getApiToken(username: String, applicationPassword: String, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "password", - "access_type": "offline", - "client_id": clientId, - "username": username, - "password": applicationPassword - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - public func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) { var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) From 9a7940c9e4b47c306784e131d1159f1d91c13ec6 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 10 Jan 2024 08:11:45 +0100 Subject: [PATCH 21/27] refactor: Remove InfomaniakNetworkLogin from core --- Package.resolved | 9 + Package.swift | 2 + Sources/InfomaniakCore/Account/Account.swift | 1 + .../Account/KeychainHelper.swift | 63 ++++--- .../Networking/ApiFetcher.swift | 9 +- .../InfomaniakCore/Networking/ApiToken.swift | 91 --------- .../Requests/InfomaniakNetworkLogin.swift | 174 ------------------ 7 files changed, 56 insertions(+), 293 deletions(-) delete mode 100644 Sources/InfomaniakCore/Networking/ApiToken.swift delete mode 100644 Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift diff --git a/Package.resolved b/Package.resolved index b5e229c..8988ebb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "2.0.1" } }, + { + "identity" : "ios-login", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Infomaniak/ios-login", + "state" : { + "branch" : "login-refactor", + "revision" : "3c48ead189a5dc0602f53e32666f32d026599200" + } + }, { "identity" : "realm-core", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 5fd0dc6..8d81176 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Infomaniak/ios-login", branch: "login-refactor"), .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")), .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.0")), @@ -29,6 +30,7 @@ let package = Package( dependencies: [ "Alamofire", .product(name: "InfomaniakDI", package: "ios-dependency-injection"), + .product(name: "InfomaniakLogin", package: "ios-login"), .product(name: "Sentry", package: "sentry-cocoa"), .product(name: "RealmSwift", package: "realm-swift"), .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), diff --git a/Sources/InfomaniakCore/Account/Account.swift b/Sources/InfomaniakCore/Account/Account.swift index 6617e74..81ad1b1 100644 --- a/Sources/InfomaniakCore/Account/Account.swift +++ b/Sources/InfomaniakCore/Account/Account.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakLogin public protocol AccountUpdateDelegate { func didUpdateCurrentAccount(_ account: Account) diff --git a/Sources/InfomaniakCore/Account/KeychainHelper.swift b/Sources/InfomaniakCore/Account/KeychainHelper.swift index ad067cf..ac434db 100644 --- a/Sources/InfomaniakCore/Account/KeychainHelper.swift +++ b/Sources/InfomaniakCore/Account/KeychainHelper.swift @@ -1,43 +1,44 @@ /* Infomaniak Core - iOS Copyright (C) 2023 Infomaniak Network SA - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program. If not, see . */ import CocoaLumberjackSwift import Foundation +import InfomaniakLogin import Sentry public class KeychainHelper { let accessGroup: String let tag = "ch.infomaniak.token".data(using: .utf8)! let keychainQueue = DispatchQueue(label: "com.infomaniak.keychain") - + let lockedKey = "isLockedKey" let lockedValue = "locked".data(using: .utf8)! var accessibilityValueWritten = false - + public init(accessGroup: String) { self.accessGroup = accessGroup } - + public var isKeychainAccessible: Bool { if !accessibilityValueWritten { initKeychainAccessibility() } - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: lockedKey, @@ -47,13 +48,13 @@ public class KeychainHelper { kSecReturnRef as String: kCFBooleanTrue as Any, kSecMatchLimit as String: kSecMatchLimitAll ] - + var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) } - + if resultCode == noErr, let array = result as? [[String: Any]] { for item in array { if let value = item[kSecValueData as String] as? Data { @@ -66,7 +67,7 @@ public class KeychainHelper { return false } } - + func initKeychainAccessibility() { accessibilityValueWritten = true let queryAdd: [String: Any] = [ @@ -81,7 +82,7 @@ public class KeychainHelper { "[Keychain] Successfully init KeychainHelper ? \(resultCode == noErr || resultCode == errSecDuplicateItem), \(resultCode)" ) } - + public func deleteToken(for userId: Int) { keychainQueue.sync { let queryDelete: [String: Any] = [ @@ -93,7 +94,7 @@ public class KeychainHelper { DDLogInfo("Successfully deleted token ? \(resultCode == noErr)") } } - + public func deleteAllTokens() { keychainQueue.sync { let queryDelete: [String: Any] = [ @@ -104,12 +105,12 @@ public class KeychainHelper { DDLogInfo("Successfully deleted all tokens ? \(resultCode == noErr)") } } - + public func storeToken(_ token: ApiToken) { var resultCode: OSStatus = noErr let tokenData = try! JSONEncoder().encode(token) - + if let savedToken = getSavedToken(for: token.userId) { keychainQueue.sync { // Save token only if it's more recent @@ -118,7 +119,7 @@ public class KeychainHelper { kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "\(token.userId)" ] - + let attributes: [String: Any] = [ kSecValueData as String: tokenData ] @@ -149,7 +150,7 @@ public class KeychainHelper { .generateBreadcrumb(level: .error, message: "Failed saving token", keychainError: resultCode)) } } - + public func getSavedToken(for userId: Int) -> ApiToken? { var savedToken: ApiToken? keychainQueue.sync { @@ -164,11 +165,11 @@ public class KeychainHelper { kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(queryFindOne as CFDictionary, UnsafeMutablePointer($0)) } - + let jsonDecoder = JSONDecoder() if resultCode == noErr, let keychainItem = result as? [String: Any], @@ -179,7 +180,7 @@ public class KeychainHelper { } return savedToken } - + public func loadTokens() -> [ApiToken] { var values = [ApiToken]() keychainQueue.sync { @@ -192,14 +193,14 @@ public class KeychainHelper { kSecReturnRef as String: kCFBooleanTrue as Any, kSecMatchLimit as String: kSecMatchLimitAll ] - + var result: AnyObject? - + let resultCode = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) } DDLogInfo("Successfully loaded tokens ? \(resultCode == noErr)") - + guard resultCode == noErr else { let crumb = Breadcrumb(level: .error, category: "Token") crumb.type = "error" @@ -208,7 +209,7 @@ public class KeychainHelper { SentrySDK.addBreadcrumb(crumb) return } - + if let array = result as? [[String: Any]] { let jsonDecoder = JSONDecoder() for item in array { @@ -226,3 +227,17 @@ public class KeychainHelper { return values } } + +public extension ApiToken { + func generateBreadcrumb(level: SentryLevel, message: String, keychainError: OSStatus = noErr) -> Breadcrumb { + let crumb = Breadcrumb(level: level, category: "Token") + crumb.type = level == .info ? "info" : "error" + crumb.message = message + crumb.data = ["User id": userId, + "Expiration date": expirationDate.timeIntervalSince1970, + "Access Token": truncatedAccessToken, + "Refresh Token": truncatedRefreshToken, + "Keychain error code": keychainError] + return crumb + } +} diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 6986538..824e53f 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -19,6 +19,7 @@ import Alamofire import Foundation import InfomaniakDI +import InfomaniakLogin import Sentry public protocol RefreshTokenDelegate: AnyObject { @@ -29,14 +30,14 @@ public protocol RefreshTokenDelegate: AnyObject { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) open class ApiFetcher { public typealias RequestModifier = (inout URLRequest) throws -> Void - + /// All status except 401 are handled by our code, 401 status is handled by Alamofire's Authenticator code private static var handledHttpStatus: Set = { var allStatus = Set(200 ... 500) allStatus.remove(401) return allStatus }() - + public var authenticatedSession: Session! public static var decoder: JSONDecoder = { let decoder = JSONDecoder() @@ -139,8 +140,8 @@ open class ApiFetcher { decoder: JSONDecoder = ApiFetcher.decoder) async throws -> (data: T, responseAt: Int?) { let validatedRequest = request.validate(statusCode: ApiFetcher.handledHttpStatus) let response = await validatedRequest.serializingDecodable(ApiResponse.self, - automaticallyCancelling: true, - decoder: decoder).response + automaticallyCancelling: true, + decoder: decoder).response let apiResponse = try response.result.get() return try handleApiResponse(apiResponse, responseStatusCode: response.response?.statusCode ?? -1) } diff --git a/Sources/InfomaniakCore/Networking/ApiToken.swift b/Sources/InfomaniakCore/Networking/ApiToken.swift deleted file mode 100644 index 71ba410..0000000 --- a/Sources/InfomaniakCore/Networking/ApiToken.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - Infomaniak Core - iOS - Copyright (C) 2023 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import Sentry - -@objc public class ApiToken: NSObject, Codable { - @objc public var accessToken: String - @objc public var expiresIn: Int - @objc public var refreshToken: String - @objc public var scope: String - @objc public var tokenType: String - @objc public var userId: Int - @objc public var expirationDate: Date - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - case userId = "user_id" - case scope - case expirationDate - } - - public required init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - accessToken = try values.decode(String.self, forKey: .accessToken) - expiresIn = try values.decode(Int.self, forKey: .expiresIn) - refreshToken = try values.decode(String.self, forKey: .refreshToken) - scope = try values.decode(String.self, forKey: .scope) - tokenType = try values.decode(String.self, forKey: .tokenType) - userId = try values.decode(Int.self, forKey: .userId) - - let newExpirationDate = Date().addingTimeInterval(TimeInterval(Double(expiresIn))) - expirationDate = try values.decodeIfPresent(Date.self, forKey: .expirationDate) ?? newExpirationDate - } - - public init(accessToken: String, expiresIn: Int, refreshToken: String, scope: String, tokenType: String, userId: Int, expirationDate: Date) { - self.accessToken = accessToken - self.expiresIn = expiresIn - self.refreshToken = refreshToken - self.scope = scope - self.tokenType = tokenType - self.userId = userId - self.expirationDate = expirationDate - } -} - -// MARK: - Token Logging - -extension ApiToken { - public var truncatedAccessToken: String { - truncateToken(accessToken) - } - - public var truncatedRefreshToken: String { - truncateToken(refreshToken) - } - - func truncateToken(_ token: String) -> String { - String(token.prefix(4) + "-*****-" + token.suffix(4)) - } - - public func generateBreadcrumb(level: SentryLevel, message: String, keychainError: OSStatus = noErr) -> Breadcrumb { - let crumb = Breadcrumb(level: level, category: "Token") - crumb.type = level == .info ? "info" : "error" - crumb.message = message - crumb.data = ["User id": userId, - "Expiration date": expirationDate.timeIntervalSince1970, - "Access Token": truncatedAccessToken, - "Refresh Token": truncatedRefreshToken, - "Keychain error code": keychainError] - return crumb - } -} diff --git a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift b/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift deleted file mode 100644 index 2edd658..0000000 --- a/Sources/InfomaniakCore/Networking/Requests/InfomaniakNetworkLogin.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - Infomaniak Core - iOS - Copyright (C) 2023 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation - -public enum Constants { - public static let LOGIN_URL = "https://login.infomaniak.com/" - public static let DELETEACCOUNT_URL = "https://manager.infomaniak.com/v3/ng/profile/user/dashboard?open-terminate-account-modal" - public static let RESPONSE_TYPE = "code" - public static let ACCESS_TYPE = "offline" - public static let HASH_MODE = "SHA-256" - public static let HASH_MODE_SHORT = "S256" - - public static func autologinUrl(to destination: String) -> URL? { - return URL(string: "https://manager.infomaniak.com/v3/mobile_login/?url=\(destination)") - } -} - -/// Something that can keep the network stack authenticated -public protocol InfomaniakNetworkLoginable { - /// Get an api token async (callback on background thread) - func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) - - /// Refresh api token async (callback on background thread) - func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) - - /// Delete an api token async - func deleteApiToken(token: ApiToken, onError: @escaping (Error) -> Void) -} - -public class InfomaniakNetworkLogin: InfomaniakNetworkLoginable { - private static let LOGIN_API_URL = "https://login.infomaniak.com/" - private static let GET_TOKEN_API_URL = LOGIN_API_URL + "token" - - private var clientId: String - private var loginBaseUrl: String - private var redirectUri: String - - // MARK: Public - - public init(clientId: String, - loginUrl: String = Constants.LOGIN_URL, - redirectUri: String = "\(Bundle.main.bundleIdentifier ?? "")://oauth2redirect") { - self.loginBaseUrl = loginUrl - self.clientId = clientId - self.redirectUri = redirectUri - } - - public func getApiTokenUsing(code: String, codeVerifier: String, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "authorization_code", - "client_id": clientId, - "code": code, - "code_verifier": codeVerifier, - "redirect_uri": redirectUri - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - - public func refreshToken(token: ApiToken, completion: @escaping (ApiToken?, Error?) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - - let parameterDictionary: [String: Any] = [ - "grant_type": "refresh_token", - "client_id": clientId, - "refresh_token": token.refreshToken - ] - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpBody = parameterDictionary.percentEncoded() - - getApiToken(request: request, completion: completion) - } - - public func deleteApiToken(token: ApiToken, onError: @escaping (Error) -> Void) { - var request = URLRequest(url: URL(string: Self.GET_TOKEN_API_URL)!) - request.addValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization") - request.httpMethod = "DELETE" - - URLSession.shared.dataTask(with: request) { data, response, sessionError in - guard let response = response as? HTTPURLResponse, let data else { - if let sessionError { - onError(sessionError) - } - return - } - - do { - if !response.isSuccessful() { - let apiDeleteToken = try JSONDecoder().decode(ApiDeleteToken.self, from: data) - onError(NSError(domain: apiDeleteToken.error!, code: response.statusCode, userInfo: ["Error": apiDeleteToken.error!])) - } - } catch { - onError(error) - } - }.resume() - } - - // MARK: Private - - /// Make the get token network call - private func getApiToken(request: URLRequest, completion: @escaping (ApiToken?, Error?) -> Void) { - let session = URLSession.shared - session.dataTask(with: request) { data, response, sessionError in - guard let response = response as? HTTPURLResponse, - let data = data, data.count > 0 else { - completion(nil, sessionError) - return - } - - do { - if response.isSuccessful() { - let apiToken = try JSONDecoder().decode(ApiToken.self, from: data) - completion(apiToken, nil) - } else { - let apiError = try JSONDecoder().decode(LoginApiError.self, from: data) - completion(nil, NSError(domain: apiError.error, code: response.statusCode, userInfo: ["Error": apiError])) - } - } catch { - completion(nil, error) - } - }.resume() - } -} - -extension HTTPURLResponse { - func isSuccessful() -> Bool { - return statusCode >= 200 && statusCode <= 299 - } -} - -extension Dictionary { - func percentEncoded() -> Data? { - return map { key, value in - let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" - let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" - return escapedKey + "=" + escapedValue - } - .joined(separator: "&") - .data(using: .utf8) - } -} - -extension CharacterSet { - static let urlQueryValueAllowed: CharacterSet = { - let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 - let subDelimitersToEncode = "!$&'()*+,;=" - - var allowed = CharacterSet.urlQueryAllowed - allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") - return allowed - }() -} From 85721ea345825d7a35929f45c15e53937462b10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 15 Jan 2024 12:53:32 +0100 Subject: [PATCH 22/27] fix: UIImage was wrongfully detected with files containing an image --- .../NSItemProvider+Detect.swift | 9 ++++----- .../InfomaniakCore/ItemProviderRepresentation/UTI.swift | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/NSItemProvider+Detect.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/NSItemProvider+Detect.swift index 39b222e..5b1e624 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/NSItemProvider+Detect.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/NSItemProvider+Detect.swift @@ -21,9 +21,8 @@ import InfomaniakDI /// Extending NSItemProvider for detecting file type, business logic. public extension NSItemProvider { - /// image identifiers supported by the app + /// image file identifiers supported by the app private static let imageUTIIdentifiers = [ - UTI.image.identifier, UTI.jpeg.identifier, UTI.tiff.identifier, UTI.gif.identifier, @@ -87,13 +86,13 @@ public extension NSItemProvider { return .isText } else if hasItemConformingToAnyOfTypeIdentifiers(Self.imageUTIIdentifiers) { return .isImageData + } else if registeredTypeIdentifiers.count == 1 && + registeredTypeIdentifiers.first == UTI.image.identifier { + return .isUIImage } else if hasItemConformingToAnyOfTypeIdentifiers(Self.directoryUTIIdentifiers) { return .isDirectory } else if hasItemConformingToAnyOfTypeIdentifiers(Self.compressedUTIIdentifiers) { return .isCompressedData(identifier: typeIdentifier) - } else if registeredTypeIdentifiers.count == 1 && - registeredTypeIdentifiers.first == UTI.image.identifier { - return .isUIImage } else { return .isMiscellaneous(identifier: typeIdentifier) } diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/UTI.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/UTI.swift index e9fb271..2984c53 100644 --- a/Sources/InfomaniakCore/ItemProviderRepresentation/UTI.swift +++ b/Sources/InfomaniakCore/ItemProviderRepresentation/UTI.swift @@ -311,6 +311,7 @@ public struct UTI: RawRepresentable { public static let webArchive = UTI(rawValue: kUTTypeWebArchive) + /// Typical for UIImage public static let image = UTI(rawValue: kUTTypeImage) public static let jpeg = UTI(rawValue: kUTTypeJPEG) From 295a7494ce939d82bc741017fe435a1f9f6d56ea Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 15 Jan 2024 13:18:11 +0100 Subject: [PATCH 23/27] chore: Upgrade dependencies --- Package.resolved | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8988ebb..5a671a9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -12,7 +12,7 @@ { "identity" : "cocoalumberjack", "kind" : "remoteSourceControl", - "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack", "state" : { "revision" : "363ed23d19a931809ea834a7d722da830353806a", "version" : "3.8.2" @@ -33,7 +33,7 @@ "location" : "https://github.com/Infomaniak/ios-login", "state" : { "branch" : "login-refactor", - "revision" : "3c48ead189a5dc0602f53e32666f32d026599200" + "revision" : "9e34ff1ab1df7f8c841f368e8982e0024883579c" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { From 52a17e290f887bfcb518c330beb58ff3dab87daa Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 16 Jan 2024 13:47:46 +0100 Subject: [PATCH 24/27] chore: Update core references --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5a671a9..cc7ad8c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-login", "state" : { - "branch" : "login-refactor", - "revision" : "9e34ff1ab1df7f8c841f368e8982e0024883579c" + "revision" : "904c1ac39b4db56212302b464a0b2e023d9b5791", + "version" : "6.0.0" } }, { diff --git a/Package.swift b/Package.swift index 8d81176..1ac21f0 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")), - .package(url: "https://github.com/Infomaniak/ios-login", branch: "login-refactor"), + .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "6.0.0")), .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")), .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.18.0")), .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.45.0")), From dc0c5266eb082d8da0bd6487cacfb0d8cb137131 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 16 Jan 2024 14:06:21 +0100 Subject: [PATCH 25/27] fix: Accomodate infinite refresh token --- .../Account/KeychainHelper.swift | 31 +++++++++++++------ .../Networking/ApiFetcher.swift | 3 ++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/InfomaniakCore/Account/KeychainHelper.swift b/Sources/InfomaniakCore/Account/KeychainHelper.swift index ac434db..7eb3974 100644 --- a/Sources/InfomaniakCore/Account/KeychainHelper.swift +++ b/Sources/InfomaniakCore/Account/KeychainHelper.swift @@ -113,19 +113,30 @@ public class KeychainHelper { if let savedToken = getSavedToken(for: token.userId) { keychainQueue.sync { + let queryUpdate: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "\(token.userId)" + ] + + let attributes: [String: Any] = [ + kSecValueData as String: tokenData + ] + // Save token only if it's more recent - if savedToken.expirationDate <= token.expirationDate { - let queryUpdate: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: "\(token.userId)" - ] - - let attributes: [String: Any] = [ - kSecValueData as String: tokenData - ] + if let savedTokenExpirationDate = savedToken.expirationDate, + let newTokenExpirationDate = token.expirationDate, + savedTokenExpirationDate <= newTokenExpirationDate { resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary) DDLogInfo("Successfully updated token ? \(resultCode == noErr)") SentrySDK.addBreadcrumb(token.generateBreadcrumb(level: .info, message: "Successfully updated token")) + } else if savedToken.expirationDate == nil || token.expirationDate == nil { + // Or if one of them is now an infinite refresh token + resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary) + DDLogInfo("Successfully updated unlimited token ? \(resultCode == noErr)") + SentrySDK.addBreadcrumb(token.generateBreadcrumb( + level: .info, + message: "Successfully updated unlimited token" + )) } } } else { @@ -234,7 +245,7 @@ public extension ApiToken { crumb.type = level == .info ? "info" : "error" crumb.message = message crumb.data = ["User id": userId, - "Expiration date": expirationDate.timeIntervalSince1970, + "Expiration date": expirationDate?.timeIntervalSince1970 ?? "infinite", "Access Token": truncatedAccessToken, "Refresh Token": truncatedRefreshToken, "Keychain error code": keychainError] diff --git a/Sources/InfomaniakCore/Networking/ApiFetcher.swift b/Sources/InfomaniakCore/Networking/ApiFetcher.swift index 824e53f..48b46a1 100644 --- a/Sources/InfomaniakCore/Networking/ApiFetcher.swift +++ b/Sources/InfomaniakCore/Networking/ApiFetcher.swift @@ -215,6 +215,9 @@ open class OAuthAuthenticator: Authenticator { extension ApiToken: AuthenticationCredential { public var requiresRefresh: Bool { + guard let expirationDate else { + return false + } return Date() > expirationDate } } From 7ecbb05510b87b31c6f3f4f527dbbd8fcddb7caf Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 18 Jan 2024 09:40:00 +0100 Subject: [PATCH 26/27] feat: Identifiable collection --- .../Extensions/Collection+Identity.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Sources/InfomaniakCore/Extensions/Collection+Identity.swift diff --git a/Sources/InfomaniakCore/Extensions/Collection+Identity.swift b/Sources/InfomaniakCore/Extensions/Collection+Identity.swift new file mode 100644 index 0000000..2250f28 --- /dev/null +++ b/Sources/InfomaniakCore/Extensions/Collection+Identity.swift @@ -0,0 +1,31 @@ +/* + Infomaniak Core - iOS + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import SwiftUI + +@available(iOS 13.0, *) +public extension Collection where Element: Identifiable { + /// Compute a stable id for the given collection + func collectionId(baseId: AnyHashable? = nil) -> Int { + var hasher = Hasher() + hasher.combine(baseId) + forEach { hasher.combine($0.id) } + return hasher.finalize() + } +} From c82698045fc71727da9711ed51d9562a778c2b11 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 18 Jan 2024 10:55:52 +0100 Subject: [PATCH 27/27] test: Add tests for UTCollection+Identity --- .../Extensions/UTCollection+Identity.swift | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Tests/InfomaniakCoreTests/Extensions/UTCollection+Identity.swift diff --git a/Tests/InfomaniakCoreTests/Extensions/UTCollection+Identity.swift b/Tests/InfomaniakCoreTests/Extensions/UTCollection+Identity.swift new file mode 100644 index 0000000..0a17895 --- /dev/null +++ b/Tests/InfomaniakCoreTests/Extensions/UTCollection+Identity.swift @@ -0,0 +1,107 @@ +/* + Infomaniak Core - iOS + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import InfomaniakCore +import XCTest + +extension Int: Identifiable { + public var id: Int { + return self + } +} + +@available(iOS 13.0, *) +final class UTCollectionIdentity: XCTestCase { + func testIntSameArraySameHash() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 4] + + // WHEN + let lhsId = lhsArray.collectionId() + let rhsId = rhsArray.collectionId() + + // THEN + XCTAssertEqual(lhsId, rhsId, "We expect the ids to be the same") + } + + func testIntSameArraySameHashWithBase() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 4] + + // WHEN + let lhsId = lhsArray.collectionId(baseId: 10) + let rhsId = rhsArray.collectionId(baseId: 10) + + // THEN + XCTAssertEqual(lhsId, rhsId, "We expect the ids to be the same") + } + + func testIntReversedArrayDifferentHash() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 4].reversed() + + // WHEN + let lhsId = lhsArray.collectionId() + let rhsId = rhsArray.collectionId() + + // THEN + XCTAssertNotEqual(lhsId, rhsId, "We expect the ids to not be the same") + } + + func testIntDifferentArrayDifferentHash() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 5] + + // WHEN + let lhsId = lhsArray.collectionId() + let rhsId = rhsArray.collectionId() + + // THEN + XCTAssertNotEqual(lhsId, rhsId, "We expect the ids to not be the same") + } + + func testIntDifferentArrayDifferentHashWithBase() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 5] + + // WHEN + let lhsId = lhsArray.collectionId(baseId: 10) + let rhsId = rhsArray.collectionId(baseId: 10) + + // THEN + XCTAssertNotEqual(lhsId, rhsId, "We expect the ids to not be the same") + } + + func testIntDifferentArrayDifferentHashWithDifferentBase() { + // GIVEN + let lhsArray = [1, 2, 3, 4] + let rhsArray = [1, 2, 3, 5] + + // WHEN + let lhsId = lhsArray.collectionId(baseId: 10) + let rhsId = rhsArray.collectionId(baseId: 11) + + // THEN + XCTAssertNotEqual(lhsId, rhsId, "We expect the ids to not be the same") + } +}