diff --git a/Networking/HTTPHeader+Extensions.swift b/Networking/HTTPHeader+Extensions.swift index 13fee795..70212514 100644 --- a/Networking/HTTPHeader+Extensions.swift +++ b/Networking/HTTPHeader+Extensions.swift @@ -5,13 +5,6 @@ import Alamofire extension HTTPHeader { - /// - /// Convenience method to initialize a new OCS API request header with the given value. - /// - public static func ocsAPIRequest(_ value: Bool) -> HTTPHeader { - HTTPHeader(name: "OCS-APIRequest", value: value ? "true" : "false") - } - /// /// Convenience method to initialize a new If-None-Match header with the given value. /// diff --git a/Networking/NoteSessionManager.swift b/Networking/NoteSessionManager.swift index 288cbd82..60ba6e6f 100644 --- a/Networking/NoteSessionManager.swift +++ b/Networking/NoteSessionManager.swift @@ -8,6 +8,7 @@ import Alamofire import Foundation +import NextcloudKit import UIKit import SwiftMessages import os @@ -130,39 +131,23 @@ class NoteSessionManager { session = Session(configuration: configuration, serverTrustManager: NotesServerTrustPolicyManager(allHostsMustBeEvaluated: true, evaluators: [:])) } - /// - /// Fetch the server status. - /// - /// - Parameters: - /// - completion: Optional completion handler to call afterwards. - /// - func status(completion: SyncCompletionBlock? = nil) { + /// Fetch the server status from `/status.php`. + func status() async { logger.notice("Fetching status...") - let router = StatusRouter.status - session - .request(router) - .validate(contentType: [Router.applicationJson]) - .responseDecodable(of: CloudStatus.self) { response in - switch response.result { - case let .success(result): - KeychainHelper.productVersion = result.versionstring - KeychainHelper.productName = result.productname - case let .failure(error): - print(error.localizedDescription) - } - completion?() + guard let serverUrl = canonicalServerComponents(from: KeychainHelper.server)?.url?.absoluteString else { + logger.error("Cannot fetch status: server URL is empty or invalid.") + return } - } - /// - /// Asynchronous wrapper for ``status(completion:)``. - /// - func status() async { - await withCheckedContinuation { continuation in - status { - continuation.resume() - } + let (_, result) = await NextcloudKit.shared.getServerStatusAsync(serverUrl: serverUrl) + + switch result { + case let .success(info): + KeychainHelper.productVersion = info.version + KeychainHelper.productName = info.productName + case let .failure(error): + logger.error("Error fetching status: \(error.errorDescription, privacy: .public)") } } diff --git a/Networking/OCSRouter.swift b/Networking/OCSRouter.swift deleted file mode 100644 index 2166e116..00000000 --- a/Networking/OCSRouter.swift +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Iva Horn -// SPDX-License-Identifier: GPL-3.0-or-later - -import Alamofire -import Foundation - -/// -/// Network router for server capabilities endpoint. -/// -/// > Notice: This implementation might be obsolete and redundant because of available NextcloudKit features. -/// -enum OCSRouter: URLRequestConvertible { - case capabilities - - var method: HTTPMethod { - switch self { - case .capabilities: - return .get - } - } - - var path: String { - switch self { - case .capabilities: - return "/capabilities" - } - } - - func asURLRequest() throws -> URLRequest { - let serverAddress = KeychainHelper.server - - guard serverAddress.isEmpty == false else { - throw AFError.parameterEncodingFailed(reason: .missingURL) - } - - var endpointComponents = URLComponents() - - if let serverAddressURL = URL(string: serverAddress), - let serverAddressComponents = URLComponents(url: serverAddressURL, resolvingAgainstBaseURL: false) { - endpointComponents.scheme = serverAddressComponents.scheme - endpointComponents.host = serverAddressComponents.host - endpointComponents.port = serverAddressComponents.port - endpointComponents.path = serverAddressComponents.path - - var serverAddressPathComponents = serverAddressURL.pathComponents - - if serverAddressPathComponents.last == "index.php" { - serverAddressPathComponents = serverAddressPathComponents.dropLast() - } - - var sanitizedPath = serverAddressPathComponents.joined(separator: "/") - - if sanitizedPath.last == "/" { - sanitizedPath = String(sanitizedPath.dropLast()) - } - - if sanitizedPath.hasPrefix("//") { - sanitizedPath = String(sanitizedPath.dropFirst()) - } - - endpointComponents.path = [sanitizedPath, "ocs/v1.php/cloud", self.path].joined(separator: "/") - } - - let url = try endpointComponents.url ?? serverAddress.asURL() - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = self.method.rawValue - - let username = KeychainHelper.username - let password = KeychainHelper.password - - urlRequest.headers = [ - .authorization(username: username, password: password), - .accept(Router.applicationJson), - .ocsAPIRequest(true) - ] - - return urlRequest - } -} - - diff --git a/Networking/Router.swift b/Networking/Router.swift index 89a02e78..7c3a8a18 100644 --- a/Networking/Router.swift +++ b/Networking/Router.swift @@ -73,10 +73,18 @@ enum Router: URLRequestConvertible { throw error } - let baseURLString = "\(server)/index.php/apps/notes/api/v\(apiVersion)" - let url = try baseURLString.asURL() + guard var components = canonicalServerComponents(from: server) else { + throw AFError.parameterEncodingFailed(reason: .missingURL) + } + + let basePath = components.path + components.path = basePath + "/index.php/apps/notes/api/v\(apiVersion)" + self.path + + guard let url = components.url else { + throw AFError.parameterEncodingFailed(reason: .missingURL) + } - var urlRequest = URLRequest(url: url.appendingPathComponent(self.path)) + var urlRequest = URLRequest(url: url) urlRequest.httpMethod = self.method.rawValue let username = KeychainHelper.username let password = KeychainHelper.password diff --git a/Networking/ServerStatus.swift b/Networking/ServerStatus.swift index a57ea79c..7ebb23bd 100644 --- a/Networking/ServerStatus.swift +++ b/Networking/ServerStatus.swift @@ -20,9 +20,16 @@ class ServerStatus: NSObject { } func check() async throws { - let router = StatusRouter.status + guard var components = canonicalServerComponents(from: KeychainHelper.server) else { + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorBadURL) + } + components.path = components.path + "/status.php" + guard let url = components.url else { + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorBadURL) + } + do { - let (_, _) = try await session.data(for: router.asURLRequest()) + let (_, _) = try await session.data(for: URLRequest(url: url)) } catch(let error) { throw error as NSError } diff --git a/Networking/StatusRouter.swift b/Networking/StatusRouter.swift deleted file mode 100644 index 7a22a7ae..00000000 --- a/Networking/StatusRouter.swift +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Iva Horn -// SPDX-License-Identifier: GPL-3.0-or-later - -import Alamofire -import Foundation - -/// -/// Network router for server status endpoint. -/// -/// > Notice: This implementation might be obsolete and redundant because of available NextcloudKit features. -/// -enum StatusRouter: URLRequestConvertible { - case status - - var method: HTTPMethod { - switch self { - case .status: - return .get - } - } - - var path: String { - switch self { - case .status: - return "status.php" - } - } - - func asURLRequest() throws -> URLRequest { - let serverAddress = KeychainHelper.server - - guard serverAddress.isEmpty == false else { - throw AFError.parameterEncodingFailed(reason: .missingURL) - } - - var endpointComponents = URLComponents() - - if let serverAddressURL = URL(string: serverAddress), - let serverAddressComponents = URLComponents(url: serverAddressURL, resolvingAgainstBaseURL: false) { - endpointComponents.scheme = serverAddressComponents.scheme - endpointComponents.host = serverAddressComponents.host - endpointComponents.port = serverAddressComponents.port - endpointComponents.path = serverAddressComponents.path - - var serverAddressPathComponents = serverAddressURL.pathComponents - - if serverAddressPathComponents.last == "index.php" { - serverAddressPathComponents = serverAddressPathComponents.dropLast() - } - - var sanitizedPath = serverAddressPathComponents.joined(separator: "/") - - if sanitizedPath.last == "/" { - sanitizedPath = String(sanitizedPath.dropLast()) - } - - if sanitizedPath.hasPrefix("//") { - sanitizedPath = String(sanitizedPath.dropFirst()) - } - - endpointComponents.path = [sanitizedPath, self.path].joined(separator: "/") - } - - let url = try endpointComponents.url ?? serverAddress.asURL() - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = self.method.rawValue - - let username = KeychainHelper.username - let password = KeychainHelper.password - - urlRequest.headers = [ - .authorization(username: username, password: password), - .accept(Router.applicationJson), - .ocsAPIRequest(true) - ] - - return urlRequest - } -} - diff --git a/Utils/UtilityExtensions.swift b/Utils/UtilityExtensions.swift index 995c5dca..1770c51c 100644 --- a/Utils/UtilityExtensions.swift +++ b/Utils/UtilityExtensions.swift @@ -56,7 +56,6 @@ extension UIColor { static let ph_popoverBackgroundColor = UIColor(named: "PHWhitePopoverBackground")! static let ph_popoverButtonColor = UIColor(named: "PHWhitePopoverButton")! static let ph_popoverBorderColor = UIColor(named: "PHWhitePopoverBorder")! -// static let ph_popoverIconColor = UIColor(named: "PHWhitePopoverIcon")! static let ph_switchTintColor = UIColor(named: "PHWhitePopoverBorder")! static let ph_selectedTextColor = UIColor(named: "PHSelectedText")! @@ -142,3 +141,15 @@ func isNextcloud() -> Bool { } catch { } return isNextcloud } + +/// Returns components for `serverAddress` with any trailing `/index.php` or `/` removed. +/// Handles cases where the address may have been received in a bad format, ex. test.com/nextcloud/index.php/index.php +func canonicalServerComponents(from serverAddress: String) -> URLComponents? { + guard let url = URL(string: serverAddress) else { return nil } + let stripped = url.lastPathComponent == "index.php" ? url.deletingLastPathComponent() : url + guard var components = URLComponents(url: stripped, resolvingAgainstBaseURL: false) else { return nil } + if components.path.hasSuffix("/") { + components.path = String(components.path.dropLast()) + } + return components +} diff --git a/iOCNotes.xcodeproj/project.pbxproj b/iOCNotes.xcodeproj/project.pbxproj index 5983be00..f00a0cfb 100644 --- a/iOCNotes.xcodeproj/project.pbxproj +++ b/iOCNotes.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ AA182D002DDCD6520058C246 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = AA182CFF2DDCD6520058C246 /* CodeScanner */; }; - AA3054382E12AF9B00D33159 /* StatusRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3054372E12AF9800D33159 /* StatusRouter.swift */; }; - AA30543A2E12AFFD00D33159 /* OCSRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3054392E12AFFB00D33159 /* OCSRouter.swift */; }; AA30543C2E12B06F00D33159 /* HTTPHeader+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA30543B2E12B06700D33159 /* HTTPHeader+Extensions.swift */; }; AA83B8AA2DEF080700F35F3E /* SwiftNextcloudUI in Frameworks */ = {isa = PBXBuildFile; productRef = AA83B8A92DEF080700F35F3E /* SwiftNextcloudUI */; }; AAA900B42DE6F37400D3EF0F /* SwiftNextcloudUI in Frameworks */ = {isa = PBXBuildFile; productRef = AAA900B32DE6F37400D3EF0F /* SwiftNextcloudUI */; }; @@ -113,8 +111,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - AA3054372E12AF9800D33159 /* StatusRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusRouter.swift; sourceTree = ""; }; - AA3054392E12AFFB00D33159 /* OCSRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCSRouter.swift; sourceTree = ""; }; AA30543B2E12B06700D33159 /* HTTPHeader+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+Extensions.swift"; sourceTree = ""; }; AA360F442D71E6EF00BDC831 /* iOCNotes.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = iOCNotes.xcconfig; sourceTree = ""; }; AA592A6E2DBBBA3B003A3057 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; @@ -282,8 +278,6 @@ isa = PBXGroup; children = ( AA30543B2E12B06700D33159 /* HTTPHeader+Extensions.swift */, - AA3054392E12AFFB00D33159 /* OCSRouter.swift */, - AA3054372E12AF9800D33159 /* StatusRouter.swift */, D07F5E8D2207B6FF00528E90 /* Router.swift */, D01067DD220BCE3C0047E090 /* NoteSessionManager.swift */, D0C1C39E2462448D00AAC4A8 /* SyncOperation.swift */, @@ -699,7 +693,6 @@ F3E3C8DC2C6B7DA600A80504 /* UIColor+Extension.swift in Sources */, F3E3C8D92C6B7C2200A80504 /* NCBrand.swift in Sources */, D095384A1D3313F8006BB78E /* PreviewViewController.swift in Sources */, - AA30543A2E12AFFD00D33159 /* OCSRouter.swift in Sources */, D0E60F152772D809009CF78F /* SettingsProtocol.swift in Sources */, BDD015A32353F7D4000BA001 /* PBHSplitViewController.swift in Sources */, D095AE45245BB25F00A7EF62 /* NoteSessionManager.swift in Sources */, @@ -733,7 +726,6 @@ D059F7DB1D40596D00C252F2 /* NoteExporter.swift in Sources */, BD86DD8522B9CDD500115E5D /* KeychainHelper.swift in Sources */, F3F7345B2C6FAD80007C8C0B /* BaseUIVIewController.swift in Sources */, - AA3054382E12AF9B00D33159 /* StatusRouter.swift in Sources */, D08713ED1D18DA40001EAF82 /* HeaderTextView.swift in Sources */, D0E60F5A277FADD5009CF78F /* UniversalTypes.swift in Sources */, D0E60F28277801F8009CF78F /* PreviewWebView.swift in Sources */, diff --git a/iOCNotes/Store.swift b/iOCNotes/Store.swift index 22967839..d4851a45 100644 --- a/iOCNotes/Store.swift +++ b/iOCNotes/Store.swift @@ -172,17 +172,8 @@ final class Store: Logging, Storing { return } - guard let parsedComponents = URLComponents(string: KeychainHelper.server) else { - accounts = [] - return - } - - var assembledComponents = URLComponents() - assembledComponents.scheme = parsedComponents.scheme - assembledComponents.host = parsedComponents.host - assembledComponents.port = parsedComponents.port - - guard let baseURL = assembledComponents.url?.absoluteString else { + guard let assembledComponents = canonicalServerComponents(from: KeychainHelper.server), + let baseURL = assembledComponents.url?.absoluteString else { accounts = [] return } diff --git a/iOCNotes/Views/SettingsView.swift b/iOCNotes/Views/SettingsView.swift index 6baa70fc..dd67f792 100644 --- a/iOCNotes/Views/SettingsView.swift +++ b/iOCNotes/Views/SettingsView.swift @@ -150,6 +150,7 @@ struct SettingsView: View { Text("Enter a name for the folder where notes should be saved on the server") } .navigationTitle("Settings") + .toolbarTitleDisplayMode(.inline) } } }