diff --git a/Model/Accounts/AccountsBridge.swift b/Model/Accounts/AccountsBridge.swift index 70fe94e3..8997dade 100644 --- a/Model/Accounts/AccountsBridge.swift +++ b/Model/Accounts/AccountsBridge.swift @@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge { return nil } + // Parse the urlString to check for embedded username and password + var sanitizedUrlString = value.urlString + if var urlComponents = URLComponents(string: value.urlString) { + if let user = urlComponents.user, let password = urlComponents.password { + // Sanitize the embedded username and password + let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user + let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password + + // Update the URL components with sanitized credentials + urlComponents.user = sanitizedUser + urlComponents.password = sanitizedPassword + + // Reconstruct the sanitized URL + sanitizedUrlString = urlComponents.string ?? value.urlString + } + } + return [ "id": value.id, "instanceID": value.instanceID ?? "", "name": value.name, - "apiURL": value.urlString, + "apiURL": sanitizedUrlString, "username": value.username, "password": value.password ?? "" ] diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index c3c1084e..edb854a9 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } func feed(_ page: Int?) -> Resource? { - resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed") + resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed") .withParam("page", String(page ?? 1)) } var feed: Resource? { - resource(baseURL: account.url, path: basePathAppending("auth/feed")) + resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed")) } var subscriptions: Resource? { - resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) + resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) } func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) { - resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) + resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) .child(channelID) .request(.post) .onCompletion { _ in onCompletion() } } func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) { - resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) + resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions")) .child(channelID) .request(.delete) .onCompletion { _ in onCompletion() } @@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { return nil } - return resource(baseURL: account.url, path: basePathAppending("auth/playlists")) + return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists")) } func playlist(_ id: String) -> Resource? { - resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)")) + resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)")) } func playlistVideos(_ id: String) -> Resource? { @@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { urlComponents.scheme = instanceURLComponents.scheme urlComponents.host = instanceURLComponents.host + urlComponents.user = instanceURLComponents.user + urlComponents.password = instanceURLComponents.password + urlComponents.port = instanceURLComponents.port guard let url = urlComponents.url else { return nil @@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { ) } + // Determines if the request requires Basic Auth credentials to be removed + private func needsBasicAuthRemoval(for path: String) -> Bool { + return path.hasPrefix("\(Self.basePath)/auth/") + } + + // Creates a resource URL with consideration for removing Basic Auth credentials + private func createResourceURL(baseURL: URL, path: String) -> URL { + var resourceURL = baseURL + + // Remove Basic Auth credentials if required + if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) { + urlComponents.user = nil + urlComponents.password = nil + resourceURL = urlComponents.url ?? baseURL + } + + return resourceURL.appendingPathComponent(path) + } + + func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource { + let sanitizedURL = createResourceURL(baseURL: baseURL, path: path) + return super.resource(absoluteURL: sanitizedURL) + } + private func extractThumbnails(from details: JSON) -> [Thumbnail] { details["videoThumbnails"].arrayValue.compactMap { json in guard let url = json["url"].url, @@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { return nil } - // some of instances are not configured properly and return thumbnails links - // with incorrect scheme + // Some instances are not configured properly and return thumbnail links + // with an incorrect scheme or a missing port. components.scheme = accountUrlComponents.scheme + components.port = accountUrlComponents.port + + // If basic HTTP authentication is used, + // the username and password need to be prepended to the URL. + components.user = accountUrlComponents.user + components.password = accountUrlComponents.password guard let thumbnailUrl = components.url else { return nil } + print("Final thumbnail URL: \(thumbnailUrl)") return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!) } diff --git a/Shared/Settings/InstanceForm.swift b/Shared/Settings/InstanceForm.swift index c78e1056..c74bb052 100644 --- a/Shared/Settings/InstanceForm.swift +++ b/Shared/Settings/InstanceForm.swift @@ -86,6 +86,7 @@ struct InstanceForm: View { .autocapitalization(.none) .keyboardType(.URL) #endif + .disableAutocorrection(true) #if os(tvOS) VStack { diff --git a/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (iOS).xcscheme b/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (iOS).xcscheme index dccf93ab..f526290d 100644 --- a/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (iOS).xcscheme +++ b/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (iOS).xcscheme @@ -66,6 +66,11 @@ value = "Yes" isEnabled = "YES"> + +