diff --git a/CommonsAPI/Sources/CommonsAPI/API.swift b/CommonsAPI/Sources/CommonsAPI/API.swift index f1bf9c6..dfc83ae 100644 --- a/CommonsAPI/Sources/CommonsAPI/API.swift +++ b/CommonsAPI/Sources/CommonsAPI/API.swift @@ -28,14 +28,13 @@ public actor API { let createAccountRedirectURL = URL(string: "https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes")! private(set) var userAgent: String - var referer: String - + private(set) var referer: String #if DEBUG - let urlSession = URLSessionProxy(configuration: URLSessionConfiguration.default) + let urlSession: URLSessionProxy #else - let urlSession = URLSession(configuration: URLSessionConfiguration.default) + let urlSession: URLSession #endif - + private lazy var jsonDecoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 @@ -43,9 +42,15 @@ public actor API { }() - public init(userAgent: String, referer: String) { + public init(config: URLSessionConfiguration, userAgent: String, referer: String) { self.userAgent = userAgent self.referer = referer +#if DEBUG + urlSession = URLSessionProxy(configuration: config) +#else + urlSession = URLSession(configuration: config) +#endif + // Un-Comment the following code block to test EmailAuth via email-code (https://www.mediawiki.org/wiki/Help:Extension:EmailAuth) //#if DEBUG @@ -76,7 +81,11 @@ public actor API { // var eventMonitors: [any EventMonitor] = [AlamofireNotifications()] -} + } + + public func setReferer(_ newReferer: String) { + referer = newReferer + } private func parse(_ type: T.Type, from data: Data, response: URLResponse) throws -> T { guard let http = response as? HTTPURLResponse else { @@ -175,12 +184,11 @@ public actor API { "password": password, "rememberMe": "1" ] - var request = try POST(url: commonsEndpoint, form: form) - // Optional: Referer can help in some CSRF contexts; generally not required for clientlogin. - request.setValue("https://commons.wikimedia.org/wiki/Special:UserLogin", forHTTPHeaderField: "Referer") + let request = try POST(url: commonsEndpoint, form: form) let (data, response) = try await urlSession.data(for: request) let wrapped = try parse(LoginResponseWrapped.self, from: data, response: response) + HTTPCookieStorage.shared.cloneCentralAuthCookies() return wrapped.clientlogin } @@ -205,8 +213,7 @@ public actor API { "captchaId": captchaID, "email": email ] - var request = try POST(url: commonsEndpoint, form: form) - request.setValue("https://commons.wikimedia.org/wiki/Special:CreateAccount", forHTTPHeaderField: "Referer") + let request = try POST(url: commonsEndpoint, form: form) let (data, response) = try await urlSession.data(for: request) let wrapped = try parse(CreateAccountResponseWrapped.self, from: data, response: response) @@ -237,11 +244,11 @@ public actor API { "token": emailCode, "logincontinue": "1" ] - var request = try POST(url: commonsEndpoint, form: form) - request.setValue("https://commons.wikimedia.org/wiki/Special:UserLogin", forHTTPHeaderField: "Referer") + let request = try POST(url: commonsEndpoint, form: form) let (data, response) = try await urlSession.data(for: request) let wrapped = try parse(LoginResponseWrapped.self, from: data, response: response) + HTTPCookieStorage.shared.cloneCentralAuthCookies() return wrapped.clientlogin } @@ -258,8 +265,7 @@ public actor API { "OATHToken": twoFactorCode, "logincontinue": "1" ] - var request = try POST(url: commonsEndpoint, form: form) - request.setValue("https://commons.wikimedia.org/wiki/Special:UserLogin", forHTTPHeaderField: "Referer") + let request = try POST(url: commonsEndpoint, form: form) let (data, response) = try await urlSession.data(for: request) let wrapped = try parse(LoginResponseWrapped.self, from: data, response: response) @@ -281,6 +287,7 @@ public actor API { let (data, response) = try await urlSession.data(for: request) let responseValue = try parse(ValidatePasswordResponse.self, from: data, response: response) + HTTPCookieStorage.shared.cloneCentralAuthCookies() return UsernamePasswordValidation(withRawResponse: responseValue) } @@ -1377,6 +1384,7 @@ LIMIT \(limit) ) continuation.yield(.published) + HTTPCookieStorage.shared.cloneCentralAuthCookies() continuation.finish() } catch let urlError as URLError { logger.error("Failed uploading a file due to a url error: \(urlError.errorCode) \(urlError.localizedDescription)") diff --git a/CommonsAPI/Sources/CommonsAPI/Domain.swift b/CommonsAPI/Sources/CommonsAPI/Domain.swift new file mode 100644 index 0000000..b979ffb --- /dev/null +++ b/CommonsAPI/Sources/CommonsAPI/Domain.swift @@ -0,0 +1,54 @@ +// +// Domain.swift +// CommonsFinder +// +// Created by Tom Brewe on 09.02.26. +// + +// Partly copied from Wikipedia iOS app repo + + public struct Domain { + public static let wikipedia = "wikipedia.org" + public static let wikidata = "wikidata.org" + public static let commons = "commons.wikimedia.org" + public static let mediaWiki = "www.mediawiki.org" + public static let wikispecies = "species.wikimedia.org" + public static let englishWikipedia = "en.wikipedia.org" + public static let testWikipedia = "test.wikipedia.org" + public static let wikimedia = "wikimedia.org" + public static let metaWiki = "meta.wikimedia.org" + public static let wikimediafoundation = "wikimediafoundation.org" + public static let uploads = "upload.wikimedia.org" + public static let wikibooks = "wikibooks.org" + public static let wiktionary = "wiktionary.org" + public static let wikiquote = "wikiquote.org" + public static let wikisource = "wikisource.org" + public static let wikinews = "wikinews.org" + public static let wikiversity = "wikiversity.org" + public static let wikivoyage = "wikivoyage.org" + + static let centralAuthCookieSourceDomain = commons.withDotPrefix + + static let centralAuthCookieTargetDomains = [ + Domain.wikimedia.withDotPrefix, + Domain.commons.withDotPrefix, + Domain.wikidata.withDotPrefix, + + Domain.mediaWiki.withDotPrefix, + Domain.wiktionary.withDotPrefix, + Domain.wikiquote.withDotPrefix, + Domain.wikibooks.withDotPrefix, + Domain.wikisource.withDotPrefix, + Domain.wikinews.withDotPrefix, + Domain.wikiversity.withDotPrefix, + Domain.wikispecies.withDotPrefix, + Domain.wikivoyage.withDotPrefix, + Domain.metaWiki.withDotPrefix + ] + } + +private extension String { + var withDotPrefix: String { + return "." + self + } +} diff --git a/CommonsAPI/Sources/CommonsAPI/HTTPCookieStorage+helpers.swift b/CommonsAPI/Sources/CommonsAPI/HTTPCookieStorage+helpers.swift new file mode 100644 index 0000000..5805566 --- /dev/null +++ b/CommonsAPI/Sources/CommonsAPI/HTTPCookieStorage+helpers.swift @@ -0,0 +1,62 @@ +// +// CookieStorage+helpers.swift +// CommonsFinder +// +// Created by Tom Brewe on 09.02.26. +// + +import Foundation + +extension HTTPCookieStorage { + // NOTE: These helpers are more or less directly from the Wikipedia iOS app, see for reference. + + func cloneCentralAuthCookies() { + // centralauth_ cookies work for any central auth domain - this call copies the centralauth_* cookies from .wikipedia.org to an explicit list of domains. This is hardcoded because we only want to copy ".wikipedia.org" cookies regardless of WMFDefaultSiteDomain + + + // NOTE: the auth cookies appear to be on the wikimedia.commons.org domain WITHOUT a prefixed "." + copyCookiesWithNamePrefix( + "centralauth_", + for: Domain.commons, + to: Domain.centralAuthCookieTargetDomains + ) + } + + func cookiesWithNamePrefix(_ prefix: String, for domain: String) -> [HTTPCookie] { + guard let cookies, !cookies.isEmpty else { + return [] + } + let standardizedPrefix = prefix.lowercased().precomposedStringWithCanonicalMapping + let standardizedDomain = domain.lowercased().precomposedStringWithCanonicalMapping + return cookies.filter { cookie in + cookie.domain.lowercased().precomposedStringWithCanonicalMapping == standardizedDomain && + cookie.name.lowercased().precomposedStringWithCanonicalMapping.hasPrefix(standardizedPrefix) + } + } + + func cookieWithName(_ name: String, for domain: String) -> HTTPCookie? { + guard let cookies, !cookies.isEmpty else { + return nil + } + let standardizedName = name.lowercased().precomposedStringWithCanonicalMapping + let standardizedDomain = domain.lowercased().precomposedStringWithCanonicalMapping + return cookies.filter { cookie in + cookie.domain.lowercased().precomposedStringWithCanonicalMapping == standardizedDomain && + cookie.name.lowercased().precomposedStringWithCanonicalMapping == standardizedName + }.first + } + + func copyCookiesWithNamePrefix(_ prefix: String, for domain: String, to toDomains: [String]) { + let cookies = cookiesWithNamePrefix(prefix, for: domain) + for toDomain in toDomains { + for cookie in cookies { + var properties = cookie.properties ?? [:] + properties[.domain] = toDomain + guard let copiedCookie = HTTPCookie(properties: properties) else { + continue + } + setCookie(copiedCookie) + } + } + } +} diff --git a/CommonsFinder.xcodeproj/project.pbxproj b/CommonsFinder.xcodeproj/project.pbxproj index c4f7e4d..737ac2b 100644 --- a/CommonsFinder.xcodeproj/project.pbxproj +++ b/CommonsFinder.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ E2035C452D52544D0079235B /* CommonsAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E2035C442D52544D0079235B /* CommonsAPI */; }; E206777A2EBE52FF00981A79 /* H3kit in Frameworks */ = {isa = PBXBuildFile; productRef = E20677792EBE52FF00981A79 /* H3kit */; }; E213C6B92EA29F570085C60E /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = E213C6B82EA29F570085C60E /* SwiftSoup */; }; - E22A913C2CF8B1AA00D3B8F9 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E22A913B2CF8B1AA00D3B8F9 /* Nuke */; }; - E22A913E2CF8B1AA00D3B8F9 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E22A913D2CF8B1AA00D3B8F9 /* NukeUI */; }; E2390F962EBCC64C0013F0FC /* ObservableLRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = E2390F952EBCC64C0013F0FC /* ObservableLRUCache */; }; E2390F982EBCC7250013F0FC /* ObservableLRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = E2390F972EBCC7250013F0FC /* ObservableLRUCache */; }; E24D3BFD2CF3ACDC003484CC /* FrameUp in Frameworks */ = {isa = PBXBuildFile; productRef = E24D3BFC2CF3ACDC003484CC /* FrameUp */; }; @@ -33,6 +31,8 @@ E2E2BFD52D8718AE00751949 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E2E2BFD42D8718AE00751949 /* Algorithms */; }; E2F19BC62CA43C6400E19DCD /* SwiftSecurity in Frameworks */ = {isa = PBXBuildFile; productRef = E2F19BC52CA43C6400E19DCD /* SwiftSecurity */; }; E2F1B9D52CD006C700410991 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = E2F1B9D42CD006C700410991 /* GRDB */; }; + E2F4FB322F39081700792DEE /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E2F4FB312F39081700792DEE /* Nuke */; }; + E2F4FB342F39081700792DEE /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E2F4FB332F39081700792DEE /* NukeUI */; }; E2FF0A2E2D9EAA29008F915C /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = E25278722CB03FAF00D00640 /* GRDB */; }; E2FF0A2F2D9EAA29008F915C /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = E25278752CB03FBB00D00640 /* GRDBQuery */; }; /* End PBXBuildFile section */ @@ -141,7 +141,6 @@ Database/Model/MediaFile.swift, Database/Model/MediaFileDraft.swift, Database/Model/MediaFileInfo.swift, - DataFetching/DataAccess.swift, "Generic Extensions/Array+popFirstN.swift", "Generic Extensions/Array+safeIndex.swift", "Generic Extensions/CGPoint+Extensions.swift", @@ -160,6 +159,12 @@ "Generic Extensions/Uint64+Identifiable.swift", "Generic Extensions/URL+resizedCommonsImageURL.swift", "Generic Extensions/URL+staticUrls.swift", + Networking/APIUtils.swift, + Networking/Authentication.swift, + Networking/DataAccess.swift, + Networking/Domain.swift, + "Networking/HTTPCookieStorage+helpers.swift", + Networking/Networking.swift, "Observable Models/AccountModel.swift", "Observable Models/AttributedStringCache.swift", "Observable Models/LocationManager.swift", @@ -187,8 +192,6 @@ Types/UploadPossibleStatus.swift, Types/WikidataStatement.swift, Types/WikimediaLanguage.swift, - Utilities/APIUtils.swift, - Utilities/Authentication.swift, "Utilities/CLLocation+gpsDictionary.swift", Utilities/DraftAnalysis.swift, Utilities/ExifData.swift, @@ -196,7 +199,6 @@ Utilities/GeoVectorMath.swift, Utilities/Logging.swift, Utilities/MediaDownloading.swift, - Utilities/Networking.swift, Utilities/ValidationUtils.swift, Utilities/zippedFlatMap.swift, Views/AuthView/AuthView.swift, @@ -378,12 +380,12 @@ E2390F962EBCC64C0013F0FC /* ObservableLRUCache in Frameworks */, E26D503C2DA5A75F00621D1C /* CommonsAPI in Frameworks */, E2549B262CBD68B9005DFE14 /* Algorithms in Frameworks */, - E22A913E2CF8B1AA00D3B8F9 /* NukeUI in Frameworks */, E290323F2D9EA84700A0CD8F /* GRDBQuery in Frameworks */, - E22A913C2CF8B1AA00D3B8F9 /* Nuke in Frameworks */, E206777A2EBE52FF00981A79 /* H3kit in Frameworks */, E2F19BC62CA43C6400E19DCD /* SwiftSecurity in Frameworks */, + E2F4FB342F39081700792DEE /* NukeUI in Frameworks */, E257E6BF2D2038F90049FFE8 /* Lock in Frameworks */, + E2F4FB322F39081700792DEE /* Nuke in Frameworks */, E2C92DD32ED60D4000EAA248 /* ObservableLRUCache in Frameworks */, E28B17662CB03CBB00D6FEA0 /* GRDBQuery in Frameworks */, E24D3BFD2CF3ACDC003484CC /* FrameUp in Frameworks */, @@ -497,8 +499,6 @@ E2549B2C2CBD69C7005DFE14 /* OrderedCollections */, E2F1B9D42CD006C700410991 /* GRDB */, E24D3BFC2CF3ACDC003484CC /* FrameUp */, - E22A913B2CF8B1AA00D3B8F9 /* Nuke */, - E22A913D2CF8B1AA00D3B8F9 /* NukeUI */, E257E6BE2D2038F90049FFE8 /* Lock */, E20A8A5D2D75F1D200EA79C5 /* H3kit */, E29032392D9EA52000A0CD8F /* Pulse */, @@ -511,6 +511,8 @@ E20677792EBE52FF00981A79 /* H3kit */, E2D8C6692ECF5B8200CEBB37 /* GEOSwiftMapKit */, E2C92DD22ED60D4000EAA248 /* ObservableLRUCache */, + E2F4FB312F39081700792DEE /* Nuke */, + E2F4FB332F39081700792DEE /* NukeUI */, ); productName = CommonsFinder; productReference = E2839FE72CA2DD900053C312 /* CommonsFinder.app */; @@ -577,7 +579,6 @@ E2549B272CBD69C7005DFE14 /* XCRemoteSwiftPackageReference "swift-collections" */, E2F1B9D32CD006C700410991 /* XCRemoteSwiftPackageReference "GRDB.swift" */, E24D3BFB2CF3ACDC003484CC /* XCRemoteSwiftPackageReference "FrameUp" */, - E22A913A2CF8B1AA00D3B8F9 /* XCRemoteSwiftPackageReference "Nuke" */, E257E6BD2D2038F90049FFE8 /* XCRemoteSwiftPackageReference "Lock" */, E29032382D9EA52000A0CD8F /* XCRemoteSwiftPackageReference "Pulse" */, E290323D2D9EA84700A0CD8F /* XCRemoteSwiftPackageReference "GRDBQuery" */, @@ -586,6 +587,7 @@ E20677782EBE52FF00981A79 /* XCRemoteSwiftPackageReference "h3kit-ios" */, E2D8C6682ECF5B8200CEBB37 /* XCRemoteSwiftPackageReference "GEOSwiftMapKit" */, E2C92DD12ED60D4000EAA248 /* XCRemoteSwiftPackageReference "ObservableLRUCache" */, + E2335C672F390496008951BD /* XCLocalSwiftPackageReference "../../gits/Nuke" */, ); preferredProjectObjectVersion = 77; productRefGroup = E2839FE82CA2DD900053C312 /* Products */; @@ -1066,6 +1068,10 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ + E2335C672F390496008951BD /* XCLocalSwiftPackageReference "../../gits/Nuke" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../gits/Nuke; + }; E26D503A2DA5A75F00621D1C /* XCLocalSwiftPackageReference "CommonsAPI" */ = { isa = XCLocalSwiftPackageReference; relativePath = CommonsAPI; @@ -1089,14 +1095,6 @@ minimumVersion = 2.11.1; }; }; - E22A913A2CF8B1AA00D3B8F9 /* XCRemoteSwiftPackageReference "Nuke" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kean/Nuke"; - requirement = { - kind = exactVersion; - version = 12.8.0; - }; - }; E24D3BFB2CF3ACDC003484CC /* XCRemoteSwiftPackageReference "FrameUp" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ryanlintott/FrameUp"; @@ -1206,16 +1204,6 @@ package = E213C6B72EA29F570085C60E /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - E22A913B2CF8B1AA00D3B8F9 /* Nuke */ = { - isa = XCSwiftPackageProductDependency; - package = E22A913A2CF8B1AA00D3B8F9 /* XCRemoteSwiftPackageReference "Nuke" */; - productName = Nuke; - }; - E22A913D2CF8B1AA00D3B8F9 /* NukeUI */ = { - isa = XCSwiftPackageProductDependency; - package = E22A913A2CF8B1AA00D3B8F9 /* XCRemoteSwiftPackageReference "Nuke" */; - productName = NukeUI; - }; E2390F952EBCC64C0013F0FC /* ObservableLRUCache */ = { isa = XCSwiftPackageProductDependency; productName = ObservableLRUCache; @@ -1305,6 +1293,16 @@ package = E2F1B9D32CD006C700410991 /* XCRemoteSwiftPackageReference "GRDB.swift" */; productName = GRDB; }; + E2F4FB312F39081700792DEE /* Nuke */ = { + isa = XCSwiftPackageProductDependency; + package = E2335C672F390496008951BD /* XCLocalSwiftPackageReference "../../gits/Nuke" */; + productName = Nuke; + }; + E2F4FB332F39081700792DEE /* NukeUI */ = { + isa = XCSwiftPackageProductDependency; + package = E2335C672F390496008951BD /* XCLocalSwiftPackageReference "../../gits/Nuke" */; + productName = NukeUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E2839FDF2CA2DD900053C312 /* Project object */; diff --git a/CommonsFinder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CommonsFinder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 071abe0..76e9504 100644 --- a/CommonsFinder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CommonsFinder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cb5c9be8b92565c0934e2f2cb72e75cc1eb3702e658139fff6a7aedaf3b4ae38", + "originHash" : "81606f9c268b9de1874b1a789f24c3076cb04a21f85ab737e86b49ef94f9142c", "pins" : [ { "identity" : "frameup", @@ -82,15 +82,6 @@ "version" : "1.2.0" } }, - { - "identity" : "nuke", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kean/Nuke", - "state" : { - "revision" : "0ead44350d2737db384908569c012fe67c421e4d", - "version" : "12.8.0" - } - }, { "identity" : "observablelrucache", "kind" : "remoteSourceControl", diff --git a/CommonsFinder/CommonsFinderApp.swift b/CommonsFinder/CommonsFinderApp.swift index 26f9c54..e9812de 100644 --- a/CommonsFinder/CommonsFinderApp.swift +++ b/CommonsFinder/CommonsFinderApp.swift @@ -127,14 +127,7 @@ private func configureNukeAndPulse() { DataLoader.sharedUrlCache.memoryCapacity = 0 var pipelineConfig = ImagePipeline.Configuration() - let urlSessionConfig = URLSessionConfiguration.default - - urlSessionConfig.httpAdditionalHeaders = [ - "User-Agent": Networking.shared.userAgent, - "Referer": Networking.shared.referer, - ] - - let dataLoader = DataLoader(configuration: urlSessionConfig) + let dataLoader = DataLoader(configuration: Networking.shared.config) /// TESTING NOTE: If tests fail in Pulse package, comment out the following block and try again. #if DEBUG diff --git a/CommonsFinder/Localizable.xcstrings b/CommonsFinder/Localizable.xcstrings index 5e14a3c..40b1cea 100644 --- a/CommonsFinder/Localizable.xcstrings +++ b/CommonsFinder/Localizable.xcstrings @@ -568,6 +568,7 @@ }, "Console" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/CommonsFinder/Utilities/APIUtils.swift b/CommonsFinder/Networking/APIUtils.swift similarity index 100% rename from CommonsFinder/Utilities/APIUtils.swift rename to CommonsFinder/Networking/APIUtils.swift diff --git a/CommonsFinder/Utilities/Authentication.swift b/CommonsFinder/Networking/Authentication.swift similarity index 100% rename from CommonsFinder/Utilities/Authentication.swift rename to CommonsFinder/Networking/Authentication.swift diff --git a/CommonsFinder/DataFetching/DataAccess.swift b/CommonsFinder/Networking/DataAccess.swift similarity index 100% rename from CommonsFinder/DataFetching/DataAccess.swift rename to CommonsFinder/Networking/DataAccess.swift diff --git a/CommonsFinder/Networking/Domain.swift b/CommonsFinder/Networking/Domain.swift new file mode 100644 index 0000000..81802d5 --- /dev/null +++ b/CommonsFinder/Networking/Domain.swift @@ -0,0 +1,51 @@ +// +// Domain.swift +// CommonsFinder +// +// Created by Tom Brewe on 09.02.26. +// + +// Partly copied from Wikipedia iOS app repo + +public struct Domain { + public static let wikipedia = "wikipedia.org" + public static let wikidata = "wikidata.org" + public static let commons = "commons.wikimedia.org" + public static let mediaWiki = "www.mediawiki.org" + public static let wikispecies = "species.wikimedia.org" + public static let englishWikipedia = "en.wikipedia.org" + public static let testWikipedia = "test.wikipedia.org" + public static let wikimedia = "wikimedia.org" + public static let metaWiki = "meta.wikimedia.org" + public static let wikimediafoundation = "wikimediafoundation.org" + public static let uploads = "upload.wikimedia.org" + public static let wikibooks = "wikibooks.org" + public static let wiktionary = "wiktionary.org" + public static let wikiquote = "wikiquote.org" + public static let wikisource = "wikisource.org" + public static let wikinews = "wikinews.org" + public static let wikiversity = "wikiversity.org" + public static let wikivoyage = "wikivoyage.org" + + static let centralAuthCookieSourceDomain = commons.withDotPrefix + + static let centralAuthCookieTargetDomains = [ + Domain.mediaWiki.withDotPrefix, + Domain.wikimedia.withDotPrefix, + Domain.wiktionary.withDotPrefix, + Domain.wikiquote.withDotPrefix, + Domain.wikibooks.withDotPrefix, + Domain.wikisource.withDotPrefix, + Domain.wikinews.withDotPrefix, + Domain.wikiversity.withDotPrefix, + Domain.wikispecies.withDotPrefix, + Domain.wikivoyage.withDotPrefix, + Domain.metaWiki.withDotPrefix, + ] +} + +extension String { + fileprivate var withDotPrefix: String { + return "." + self + } +} diff --git a/CommonsFinder/Networking/HTTPCookieStorage+helpers.swift b/CommonsFinder/Networking/HTTPCookieStorage+helpers.swift new file mode 100644 index 0000000..95e447f --- /dev/null +++ b/CommonsFinder/Networking/HTTPCookieStorage+helpers.swift @@ -0,0 +1,55 @@ +// +// CookieStorage+helpers.swift +// CommonsFinder +// +// Created by Tom Brewe on 09.02.26. +// + +import Foundation + +extension HTTPCookieStorage { + // NOTE: These helpers are more or less directly from the Wikipedia iOS app, see for reference. + + func cloneCentralAuthCookies() { + // centralauth_ cookies work for any central auth domain - this call copies the centralauth_* cookies from .wikipedia.org to an explicit list of domains. This is hardcoded because we only want to copy ".wikipedia.org" cookies regardless of WMFDefaultSiteDomain + copyCookiesWithNamePrefix("centralauth_", for: Domain.centralAuthCookieSourceDomain, to: Domain.centralAuthCookieTargetDomains) + } + + func cookiesWithNamePrefix(_ prefix: String, for domain: String) -> [HTTPCookie] { + guard let cookies, !cookies.isEmpty else { + return [] + } + let standardizedPrefix = prefix.lowercased().precomposedStringWithCanonicalMapping + let standardizedDomain = domain.lowercased().precomposedStringWithCanonicalMapping + return cookies.filter { cookie in + cookie.domain.lowercased().precomposedStringWithCanonicalMapping == standardizedDomain && cookie.name.lowercased().precomposedStringWithCanonicalMapping.hasPrefix(standardizedPrefix) + } + } + + func cookieWithName(_ name: String, for domain: String) -> HTTPCookie? { + guard let cookies, !cookies.isEmpty else { + return nil + } + let standardizedName = name.lowercased().precomposedStringWithCanonicalMapping + let standardizedDomain = domain.lowercased().precomposedStringWithCanonicalMapping + return + cookies.filter { cookie in + cookie.domain.lowercased().precomposedStringWithCanonicalMapping == standardizedDomain && cookie.name.lowercased().precomposedStringWithCanonicalMapping == standardizedName + } + .first + } + + func copyCookiesWithNamePrefix(_ prefix: String, for domain: String, to toDomains: [String]) { + let cookies = cookiesWithNamePrefix(prefix, for: domain) + for toDomain in toDomains { + for cookie in cookies { + var properties = cookie.properties ?? [:] + properties[.domain] = toDomain + guard let copiedCookie = HTTPCookie(properties: properties) else { + continue + } + setCookie(copiedCookie) + } + } + } +} diff --git a/CommonsFinder/Utilities/Networking.swift b/CommonsFinder/Networking/Networking.swift similarity index 71% rename from CommonsFinder/Utilities/Networking.swift rename to CommonsFinder/Networking/Networking.swift index 7c395ce..a637907 100644 --- a/CommonsFinder/Utilities/Networking.swift +++ b/CommonsFinder/Networking/Networking.swift @@ -8,12 +8,13 @@ import CommonsAPI import Foundation -struct Networking { +final class Networking { static var shared: Networking = .init() - var referer: String + private(set) var referer: String let userAgent: String let uploadComment: String + let config: URLSessionConfiguration let api: API init() { @@ -21,17 +22,34 @@ struct Networking { let info = Bundle.main.infoDictionary let executable = (info?["CFBundleExecutable"] as? String) ?? (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? "Unknown" - let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" - let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" + // let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" + // let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown" let contactInfo = "https://github.com/nylki/CommonsFinder" userAgent = "\(executable)/\(appBuild) (\(contactInfo)) \(osNameVersion)" uploadComment = "uploaded from \(executable)/\(appBuild) \(osNameVersion)" - api = API(userAgent: userAgent, referer: referer) + let urlSessionConfig = URLSessionConfiguration.default + urlSessionConfig.httpShouldSetCookies = true + urlSessionConfig.httpCookieAcceptPolicy = .always + urlSessionConfig.httpAdditionalHeaders = [ + "User-Agent": userAgent, + // NOTE: this is just the initial referer, will be updated via setReferer(). + "Referer": referer, + ] + config = urlSessionConfig + api = API(config: urlSessionConfig, userAgent: userAgent, referer: referer) } + func setReferer(_ newReferer: String) { + referer = newReferer + Task { + await api.setReferer(newReferer) + } + } + + // Preferred format for User-Agent headers for wikimedia prohects (see: https://www.mediawiki.org/wiki/API:Etiquette#The_User-Agent_header) // / () / diff --git a/CommonsFinder/Observable Models/Navigation.swift b/CommonsFinder/Observable Models/Navigation.swift index 1fc42b2..4026cd3 100644 --- a/CommonsFinder/Observable Models/Navigation.swift +++ b/CommonsFinder/Observable Models/Navigation.swift @@ -65,9 +65,9 @@ import os.log private func updateReferer() { if let currentPath = currentPath.last { - Networking.shared.referer = "CommonsFinder://\(currentPath.refererPath)" + Networking.shared.setReferer("CommonsFinder://\(currentPath.refererPath)") } else { - Networking.shared.referer = "CommonsFinder://\(selectedTab.refererPath)" + Networking.shared.setReferer("CommonsFinder://\(selectedTab.refererPath)") } logger.debug("Referer: \(Networking.shared.referer)") }