Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions CommonsAPI/Sources/CommonsAPI/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ 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
return decoder
}()


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
Expand Down Expand Up @@ -76,7 +81,11 @@ public actor API {

// var eventMonitors: [any EventMonitor] = [AlamofireNotifications()]

}
}

public func setReferer(_ newReferer: String) {
referer = newReferer
}

private func parse<T: Decodable>(_ type: T.Type, from data: Data, response: URLResponse) throws -> T {
guard let http = response as? HTTPURLResponse else {
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)")
Expand Down
54 changes: 54 additions & 0 deletions CommonsAPI/Sources/CommonsAPI/Domain.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
62 changes: 62 additions & 0 deletions CommonsAPI/Sources/CommonsAPI/HTTPCookieStorage+helpers.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
56 changes: 27 additions & 29 deletions CommonsFinder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 */
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -187,16 +192,13 @@
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,
Utilities/GeoPlacemarkCache.swift,
Utilities/GeoVectorMath.swift,
Utilities/Logging.swift,
Utilities/MediaDownloading.swift,
Utilities/Networking.swift,
Utilities/ValidationUtils.swift,
Utilities/zippedFlatMap.swift,
Views/AuthView/AuthView.swift,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -497,8 +499,6 @@
E2549B2C2CBD69C7005DFE14 /* OrderedCollections */,
E2F1B9D42CD006C700410991 /* GRDB */,
E24D3BFC2CF3ACDC003484CC /* FrameUp */,
E22A913B2CF8B1AA00D3B8F9 /* Nuke */,
E22A913D2CF8B1AA00D3B8F9 /* NukeUI */,
E257E6BE2D2038F90049FFE8 /* Lock */,
E20A8A5D2D75F1D200EA79C5 /* H3kit */,
E29032392D9EA52000A0CD8F /* Pulse */,
Expand All @@ -511,6 +511,8 @@
E20677792EBE52FF00981A79 /* H3kit */,
E2D8C6692ECF5B8200CEBB37 /* GEOSwiftMapKit */,
E2C92DD22ED60D4000EAA248 /* ObservableLRUCache */,
E2F4FB312F39081700792DEE /* Nuke */,
E2F4FB332F39081700792DEE /* NukeUI */,
);
productName = CommonsFinder;
productReference = E2839FE72CA2DD900053C312 /* CommonsFinder.app */;
Expand Down Expand Up @@ -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" */,
Expand All @@ -586,6 +587,7 @@
E20677782EBE52FF00981A79 /* XCRemoteSwiftPackageReference "h3kit-ios" */,
E2D8C6682ECF5B8200CEBB37 /* XCRemoteSwiftPackageReference "GEOSwiftMapKit" */,
E2C92DD12ED60D4000EAA248 /* XCRemoteSwiftPackageReference "ObservableLRUCache" */,
E2335C672F390496008951BD /* XCLocalSwiftPackageReference "../../gits/Nuke" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = E2839FE82CA2DD900053C312 /* Products */;
Expand Down Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */;
Expand Down
Loading