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
6 changes: 3 additions & 3 deletions Modules/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ let package = Package(
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.13.1"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20260114"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20260122"),
.package(
url: "https://github.com/Automattic/color-studio",
revision: "bf141adc75e2769eb469a3e095bdc93dc30be8de"
Expand Down
72 changes: 72 additions & 0 deletions Modules/Sources/WordPressCore/ApiCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Foundation
import WordPressAPI
import WordPressAPIInternal
import WordPressApiCache

extension WordPressApiCache {
static func bootstrap() -> WordPressApiCache? {
let instance: WordPressApiCache? = .onDiskCache() ?? .memoryCache()
instance?.startListeningForUpdates()
return instance
}

private static func onDiskCache() -> WordPressApiCache? {
let cacheURL: URL
do {
cacheURL = try FileManager.default
.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appending(path: "app.sqlite")
} catch {
NSLog("Failed to create api cache file: \(error)")
return nil
}

if let cache = WordPressApiCache.onDiskCache(file: cacheURL) {
return cache
}

if FileManager.default.fileExists(at: cacheURL) {
do {
try FileManager.default.removeItem(at: cacheURL)

if let cache = WordPressApiCache.onDiskCache(file: cacheURL) {
return cache
}
} catch {
NSLog("Failed to delete sqlite database: \(error)")
}
}

return nil
}

private static func onDiskCache(file: URL) -> WordPressApiCache? {
let cache: WordPressApiCache
do {
cache = try WordPressApiCache(url: file)
} catch {
NSLog("Failed to create an instance: \(error)")
return nil
}

do {
_ = try cache.performMigrations()
} catch {
NSLog("Failed to migrate database: \(error)")
return nil
}

return cache
}

private static func memoryCache() -> WordPressApiCache? {
do {
let cache = try WordPressApiCache()
_ = try cache.performMigrations()
return cache
} catch {
NSLog("Failed to create memory cache: \(error)")
return nil
}
}
}
2 changes: 1 addition & 1 deletion Modules/Sources/WordPressCore/Plugins/PluginService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private extension PluginService {
let updateCheck = try await wpOrgClient.checkPluginUpdates(
// Use a fairely recent version if the actual version is unknown.
wordpressCoreVersion: wordpressCoreVersion ?? "6.6",
siteUrl: ParsedUrl.parse(input: client.rootUrl),
siteUrl: ParsedUrl.parse(input: client.siteURL.absoluteString),
plugins: plugins
)
let updateAvailable = updateCheck.plugins
Expand Down
35 changes: 30 additions & 5 deletions Modules/Sources/WordPressCore/WordPressClient.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import Foundation
import WordPressAPI
import WordPressAPIInternal
import WordPressApiCache

public actor WordPressClient {

public final actor WordPressClient: Sendable {
public let siteURL: URL
public let api: WordPressAPI
public let rootUrl: String

public init(api: WordPressAPI, rootUrl: ParsedUrl) {
private var _cache: WordPressApiCache?
public var cache: WordPressApiCache? {
get {
if _cache == nil {
_cache = WordPressApiCache.bootstrap()
}
return _cache
}
}

private var _service: WpSelfHostedService?
public var service: WpSelfHostedService? {
get {
if _service == nil, let cache {
do {
_service = try api.createSelfHostedService(cache: cache)
} catch {
NSLog("Failed to create service: \(error)")
}
}
return _service
}
}

public init(api: WordPressAPI, siteURL: URL) {
self.api = api
self.rootUrl = rootUrl.url()
self.siteURL = siteURL
}
}
7 changes: 7 additions & 0 deletions Sources/WordPressData/Swift/Blog+Plans.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@
1031] // 2y Ecommerce Plan
.contains(planID?.intValue)
}

public var supportsCoreRESTAPI: Bool {
if isHostedAtWPcom {
return isAtomic()
}
return true
}
}
37 changes: 24 additions & 13 deletions Sources/WordPressData/Swift/Blog+SelfHosted.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,57 +184,68 @@ public extension WpApiApplicationPasswordDetails {
}
}

public enum WordPressSite {
case dotCom(siteId: Int, authToken: String)
case selfHosted(blogId: TaggedManagedObjectID<Blog>, apiRootURL: ParsedUrl, username: String, authToken: String)
public enum WordPressSite: Hashable {
case dotCom(siteURL: URL, siteId: Int, authToken: String)
case selfHosted(blogId: TaggedManagedObjectID<Blog>, siteURL: URL, apiRootURL: ParsedUrl, username: String, authToken: String)

public init(blog: Blog) throws {
let siteURL = try blog.getUrl()
// Directly access the site content when available.
if let restApiRootURL = blog.restApiRootURL,
let restApiRootURL = try? ParsedUrl.parse(input: restApiRootURL),
let username = blog.username,
let authToken = try? blog.getApplicationToken() {
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: restApiRootURL, username: username, authToken: authToken)
self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: siteURL, apiRootURL: restApiRootURL, username: username, authToken: authToken)
} else if let account = blog.account, let siteId = blog.dotComID?.intValue {
// When the site is added via a WP.com account, access the site via WP.com
let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username)
self = .dotCom(siteId: siteId, authToken: authToken)
self = .dotCom(siteURL: siteURL, siteId: siteId, authToken: authToken)
} else {
// In theory, this branch should never run, because the two if statements above should have covered all paths.
// But we'll keep it here as the fallback.
let url = try blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString
let apiRootURL = try ParsedUrl.parse(input: url)
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
let url = try blog.getUrl()
let apiRootURL = try ParsedUrl.parse(input: blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString)
self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: url, apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
}
}

public var siteURL: URL {
switch self {
case let .dotCom(siteURL, _, _):
return siteURL
case let .selfHosted(_, siteURL, _, _, _):
return siteURL
}
}

public static func throughDotCom(blog: Blog) -> Self? {
guard
let siteURL = try? blog.getUrl(),
let account = blog.account,
let siteId = blog.dotComID?.intValue,
let authToken = try? account.authToken ?? WPAccount.token(forUsername: account.username)
else { return nil }

return .dotCom(siteId: siteId, authToken: authToken)
return .dotCom(siteURL: siteURL, siteId: siteId, authToken: authToken)
}

public func blog(in context: NSManagedObjectContext) throws -> Blog? {
switch self {
case let .dotCom(siteId, _):
case let .dotCom(_, siteId, _):
return try Blog.lookup(withID: siteId, in: context)
case let .selfHosted(blogId, _, _, _):
case let .selfHosted(blogId, _, _, _, _):
return try context.existingObject(with: blogId)
}
}

public func blogId(in coreDataStack: CoreDataStack) -> TaggedManagedObjectID<Blog>? {
switch self {
case let .dotCom(siteId, _):
case let .dotCom(_, siteId, _):
return coreDataStack.performQuery { context in
guard let blog = try? Blog.lookup(withID: siteId, in: context) else { return nil }
return TaggedManagedObjectID(blog)
}
case let .selfHosted(id, _, _, _):
case let .selfHosted(id, _, _, _, _):
return id
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class UserListViewModelTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()

let client = try WordPressClient(api: .init(urlSession: .shared, apiRootUrl: .parse(input: "https://example.com/wp-json"), authentication: .none), rootUrl: .parse(input: "https://example.com"))
let client = try WordPressClient(
api: .init(urlSession: .shared, apiRootUrl: .parse(input: "https://example.com/wp-json"), authentication: .none),
siteURL: URL(string: "https://example.com")!
)
service = UserService(client: client)
viewModel = await UserListViewModel(userService: service, currentUserId: 0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct ApplicationPasswordRequiredView<Content: View>: View {
} else if showLoading {
ProgressView()
} else if let site {
builder(WordPressClient(site: site))
builder(WordPressClientFactory.shared.instance(for: site))
} else {
RestApiUpgradePrompt(localizedFeatureName: localizedFeatureName) {
Task {
Expand Down
49 changes: 39 additions & 10 deletions WordPress/Classes/Networking/WordPressClient.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import Foundation
import Combine
import WordPressAPI
Expand All @@ -6,12 +7,37 @@ import WordPressCore
import WordPressData
import WordPressShared

public final class WordPressClientFactory: Sendable {
public static let shared = WordPressClientFactory()

private let instances = OSAllocatedUnfairLock<[WordPressSite: WordPressClient]>(initialState: [:])
private init() {}

public func instance(for site: WordPressSite) -> WordPressClient {
instances.withLock { dict in
if let client = dict[site] {
return client
} else {
let client = WordPressClient(site: site)
dict[site] = client
return client
}
}
}

public func reset() {
instances.withLock { dict in
dict.removeAll()
}
}
}

extension WordPressClient {
static var requestedWithInvalidAuthenticationNotification: Foundation.Notification.Name {
.init("WordPressClient.requestedWithInvalidAuthenticationNotification")
}

init(site: WordPressSite) {
fileprivate convenience init(site: WordPressSite) {
// Currently, the app supports both account passwords and application passwords.
// When a site is initially signed in with an account password, WordPress login cookies are stored
// in `URLSession.shared`. After switching the site to application password authentication,
Expand All @@ -26,15 +52,18 @@ extension WordPressClient {
let provider = WpAuthenticationProvider.dynamic(
dynamicAuthenticationProvider: AutoUpdateAuthenticationProvider(site: site, coreDataStack: ContextManager.shared)
)
let siteURL: URL
let apiRootURL: ParsedUrl
let resolver: ApiUrlResolver
switch site {
case let .dotCom(siteId, _):
case let .dotCom(url, siteId, _):
siteURL = url
apiRootURL = try! ParsedUrl.parse(input: AppEnvironment.current.wordPressComApiBase.absoluteString)
resolver = WpComDotOrgApiUrlResolver(siteId: "\(siteId)", baseUrl: .custom(apiRootURL))
case let .selfHosted(_, url, _, _):
apiRootURL = url
resolver = WpOrgSiteApiUrlResolver(apiRootUrl: url)
case let .selfHosted(_, url, apiRoot, _, _):
siteURL = url
apiRootURL = apiRoot
resolver = WpOrgSiteApiUrlResolver(apiRootUrl: apiRoot)
}
let api = WordPressAPI(
urlSession: session,
Expand All @@ -43,7 +72,7 @@ extension WordPressClient {
authenticationProvider: provider,
appNotifier: notifier,
)
self.init(api: api, rootUrl: apiRootURL)
self.init(api: api, siteURL: siteURL)
}

func installJetpack() async throws -> PluginWithEditContext {
Expand Down Expand Up @@ -74,9 +103,9 @@ private final class AutoUpdateAuthenticationProvider: @unchecked Sendable, WpDyn
self.site = site
self.coreDataStack = coreDataStack
self.authentication = switch site {
case let .dotCom(_, authToken):
case let .dotCom(_, _, authToken):
.bearer(token: authToken)
case let .selfHosted(_, _, username, authToken):
case let .selfHosted(_, _, _, username, authToken):
.init(username: username, password: authToken)
}

Expand Down Expand Up @@ -144,13 +173,13 @@ private class AppNotifier: @unchecked Sendable, WpAppNotifier {
private extension WordPressSite {
func authentication(in context: NSManagedObjectContext) -> WpAuthentication {
switch self {
case let .dotCom(siteId, _):
case let .dotCom(_, siteId, _):
guard let blog = try? Blog.lookup(withID: siteId, in: context),
let token = blog.authToken else {
return WpAuthentication.none
}
return WpAuthentication.bearer(token: token)
case let .selfHosted(blogId, _, _, _):
case let .selfHosted(blogId, _, _, _, _):
guard let blog = try? context.existingObject(with: blogId),
let username = try? blog.getUsername(),
let password = try? blog.getApplicationToken()
Expand Down
Loading