Skip to content

Commit 6c1f6cd

Browse files
wpmobilebotcrazytonylidcalhoun
authored
Merge release/26.1 into trunk (#24746)
* Always load comment data when viewing comments from the notifications (#24737) * fix: GutenbergKit uses correct REST API domain for application passwords (#24738) * fix: GutenbergKit uses correct REST API domain for application passwords The existing logic preferred application passwords over bearer tokens, but did not apply same preference when setting the `siteApiRoot` domain. The authentication header and root domain must be a compatible match in order for authentication to succeed. Failed authentication resulted in missing editor features and failed upload requests. * refactor: Improve WPCom REST API conditional logic naming and structure * fix: Expand GutenbergKit REST API configuration (#24744) * test: Assert EditorConfigurationTests REST API configuration * fix: Jetpack-connected sites without an app password use WPCOM REST API Mirror Gutenberg Mobile's authentication approach to improve stability of self-hosted, Jetpack-connected sites lacking application passwords. * Update app translations – `Localizable.strings` * Update WordPress metadata translations * Update Jetpack metadata translations * Bump version number --------- Co-authored-by: Tony Li <[email protected]> Co-authored-by: David Calhoun <[email protected]>
1 parent 53cec96 commit 6c1f6cd

File tree

72 files changed

+12673
-3683
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+12673
-3683
lines changed

Tests/KeystoneTests/Helpers/BlogBuilder.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CoreData
22
import Foundation
33
import XCTest
44
import WordPressData
5+
import WordPressShared
56

67
@testable import WordPress
78

@@ -99,12 +100,12 @@ final class BlogBuilder {
99100
return self
100101
}
101102

102-
func withAnAccount(username: String = "test_user") -> Self {
103+
func withAnAccount(username: String = "test_user", authToken: String = "authtoken") -> Self {
103104
// Add Account
104105
let account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount
105106
account.displayName = "displayName"
106107
account.username = username
107-
account.authToken = "authtoken"
108+
account.authToken = authToken
108109
blog.account = account
109110

110111
return self
@@ -224,6 +225,21 @@ final class BlogBuilder {
224225

225226
return self
226227
}
228+
229+
func withApplicationPassword(_ password: String, using keychain: KeychainAccessible = KeychainUtils()) -> Self {
230+
do {
231+
try blog.setApplicationToken(password, using: keychain)
232+
} catch {
233+
XCTFail("Failed to set application password: \(error)")
234+
}
235+
return self
236+
}
237+
238+
func with(restApiRootURL: String) -> Self {
239+
blog.restApiRootURL = restApiRootURL
240+
241+
return self
242+
}
227243
}
228244

229245
extension Blog {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import Testing
2+
import Foundation
3+
import CoreData
4+
import XCTest
5+
import WordPressData
6+
import GutenbergKit
7+
8+
@testable import WordPress
9+
10+
@Suite("EditorConfiguration Authentication Tests")
11+
struct EditorConfigurationTests {
12+
private let contextManager = ContextManager.forTesting()
13+
private let keychain = TestKeychain()
14+
15+
private var context: NSManagedObjectContext {
16+
contextManager.mainContext
17+
}
18+
19+
@Test("Simple site uses WP.com REST API")
20+
func simpleSiteUses() async throws {
21+
let context = self.context
22+
23+
let blog = BlogBuilder(context)
24+
.with(atomic: false)
25+
.isHostedAtWPcom()
26+
.withAnAccount(username: "simpleuser", authToken: "simple-bearer-token")
27+
.with(dotComID: 12345)
28+
.build()
29+
30+
let config = EditorConfiguration(blog: blog)
31+
32+
#expect(config.siteApiRoot == "https://public-api.wordpress.com/", "Should use WP.com API root")
33+
#expect(config.authHeader == "Bearer simple-bearer-token", "Should use Bearer authentication from account")
34+
}
35+
36+
@Test("Atomic site uses WP.com REST API")
37+
func atomicSiteUsesWPcomRestApi() async throws {
38+
let context = self.context
39+
40+
let blog = BlogBuilder(context)
41+
.with(atomic: true)
42+
.isNotHostedAtWPcom()
43+
.withAnAccount(username: "atomicuser", authToken: "atomic-bearer-token")
44+
.with(username: "atomicuser")
45+
.with(dotComID: 67890)
46+
.with(url: "https://atomic.com")
47+
.build()
48+
49+
let config = EditorConfiguration(blog: blog)
50+
51+
#expect(config.siteApiRoot == "https://public-api.wordpress.com/", "Should use WP.com API root")
52+
#expect(config.authHeader == "Bearer atomic-bearer-token", "Should use Bearer authentication")
53+
#expect(config.siteApiNamespace.contains("sites/67890/"), "Should include site ID namespace")
54+
}
55+
56+
@Test("Atomic site with application password uses self-hosted REST API")
57+
func atomicSiteWithAppPasswordUsesSelfHostedRestApi() async throws {
58+
let context = self.context
59+
60+
let blog = BlogBuilder(context, dotComID: 67890)
61+
.with(atomic: true)
62+
.isNotHostedAtWPcom()
63+
.withAnAccount(username: "atomicuser", authToken: "atomic-bearer-token")
64+
.with(username: "atomicuser")
65+
.withApplicationPassword("test-app-password-1234", using: keychain)
66+
.build()
67+
68+
let config = EditorConfiguration(blog: blog, keychain: keychain)
69+
let base64Credentials = "YXRvbWljdXNlcjp0ZXN0LWFwcC1wYXNzd29yZC0xMjM0" // Base64 encoding of "atomicuser:test-app-password-1234"
70+
71+
#expect(config.siteApiRoot == "https://67890.example.com/wp-json/", "Should use self-hosted API URL")
72+
#expect(config.authHeader == "Basic \(base64Credentials)", "Should use Basic authentication")
73+
#expect(config.siteApiNamespace.isEmpty, "Should not have WP.com API namespace")
74+
}
75+
76+
@Test("Self-hosted site uses self-hosted REST API")
77+
func selfHostedSiteUsesSelfHostedAPI() async throws {
78+
let context = self.context
79+
80+
let blog = BlogBuilder(context)
81+
.with(atomic: false)
82+
.isNotHostedAtWPcom()
83+
.with(username: "selfhosteduser")
84+
.with(url: "https://self-hosted.org")
85+
.withApplicationPassword("test-app-password-1234", using: keychain)
86+
.with(restApiRootURL: "https://self-hosted.org/wp-json/")
87+
.build()
88+
89+
let config = EditorConfiguration(blog: blog, keychain: keychain)
90+
let base64Credentials = "c2VsZmhvc3RlZHVzZXI6dGVzdC1hcHAtcGFzc3dvcmQtMTIzNA==" // Base64 encoding of "selfhosteduser:test-app-password-1234"
91+
92+
#expect(config.siteApiRoot == "https://self-hosted.org/wp-json/", "Should use self-hosted API URL")
93+
#expect(config.authHeader == "Basic \(base64Credentials)", "Should use Basic authentication")
94+
#expect(config.siteApiNamespace.isEmpty, "Should not have WP.com API namespace")
95+
}
96+
97+
@Test("Self-hosted site with Jetpack connection uses WP.com REST API")
98+
func selfHostedSiteWithJetpackUsesWPcomRestApi() async throws {
99+
let context = self.context
100+
101+
let blog = BlogBuilder(context, dotComID: 12345)
102+
.with(atomic: false)
103+
.isNotHostedAtWPcom()
104+
.withAnAccount(username: "selfhosteduser", authToken: "self-hosted-bearer-token")
105+
.with(username: "selfhosteduser")
106+
.with(url: "https://self-hosted.org")
107+
.build()
108+
109+
let config = EditorConfiguration(blog: blog, keychain: keychain)
110+
111+
#expect(config.siteApiRoot == "https://public-api.wordpress.com/", "Should use WP.com API root")
112+
#expect(config.authHeader == "Bearer self-hosted-bearer-token", "Should use Bearer authentication")
113+
#expect(config.siteApiNamespace.contains("sites/12345/"), "Should include site ID namespace")
114+
}
115+
116+
@Test("Self-hosted site with Jetpack connection and application password uses self-hosted REST API")
117+
func selfHostedSiteWithJetpackAndAppPasswordUsesWPcomRestApi() async throws {
118+
let context = self.context
119+
120+
let blog = BlogBuilder(context, dotComID: 12345)
121+
.with(atomic: false)
122+
.isNotHostedAtWPcom()
123+
.withAnAccount(username: "selfhosteduser", authToken: "self-hosted-bearer-token")
124+
.with(username: "selfhosteduser")
125+
.with(url: "https://self-hosted.org")
126+
.withApplicationPassword("test-app-password-1234", using: keychain)
127+
.with(restApiRootURL: "https://self-hosted.org/wp-json/")
128+
.build()
129+
130+
let config = EditorConfiguration(blog: blog, keychain: keychain)
131+
let base64Credentials = "c2VsZmhvc3RlZHVzZXI6dGVzdC1hcHAtcGFzc3dvcmQtMTIzNA==" // Base64 encoding of "selfhosteduser:test-app-password-1234"
132+
133+
#expect(config.siteApiRoot == "https://self-hosted.org/wp-json/", "Should use self-hosted API URL")
134+
#expect(config.authHeader == "Basic \(base64Credentials)", "Should use Basic authentication")
135+
#expect(config.siteApiNamespace.isEmpty, "Should not have WP.com API namespace")
136+
}
137+
}

WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
172172

173173
if RemoteFeatureFlag.newGutenberg.enabled() {
174174
GutenbergKit.EditorViewController.warmup(
175-
configuration: blog.flatMap(EditorConfiguration.init(blog:)) ?? .default
175+
configuration: blog.flatMap({ EditorConfiguration.init(blog: $0) }) ?? .default
176176
)
177177
}
178178
}

WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,17 +1001,23 @@ private extension NewGutenbergViewController {
10011001
}
10021002

10031003
extension EditorConfiguration {
1004-
init(blog: Blog) {
1004+
init(blog: Blog, keychain: KeychainAccessible = KeychainUtils()) {
10051005
let selfHostedApiUrl = blog.restApiRootURL ?? blog.url(withPath: "wp-json/")
1006-
let isWPComSite = blog.isHostedAtWPcom || blog.isAtomic()
1007-
let siteApiRoot = blog.isAccessibleThroughWPCom() && isWPComSite ? blog.wordPressComRestApi?.baseURL.absoluteString : selfHostedApiUrl
1006+
let applicationPassword = try? blog.getApplicationToken(using: keychain)
1007+
let shouldUseWPComRestApi = applicationPassword == nil && blog.isAccessibleThroughWPCom()
1008+
1009+
let siteApiRoot: String?
1010+
if applicationPassword != nil {
1011+
siteApiRoot = selfHostedApiUrl
1012+
} else {
1013+
siteApiRoot = shouldUseWPComRestApi ? blog.wordPressComRestApi?.baseURL.absoluteString : selfHostedApiUrl
1014+
}
1015+
10081016
let siteId = blog.dotComID?.stringValue
10091017
let siteDomain = blog.primaryDomainAddress
10101018
let authToken = blog.authToken ?? ""
10111019
var authHeader = "Bearer \(authToken)"
10121020

1013-
let applicationPassword = try? blog.getApplicationToken()
1014-
10151021
if let appPassword = applicationPassword, let username = blog.username {
10161022
let credentials = "\(username):\(appPassword)"
10171023
if let credentialsData = credentials.data(using: .utf8) {
@@ -1022,7 +1028,7 @@ extension EditorConfiguration {
10221028

10231029
// Must provide both namespace forms to detect usages of both forms in third-party code
10241030
var siteApiNamespace: [String] = []
1025-
if isWPComSite {
1031+
if shouldUseWPComRestApi {
10261032
if let siteId {
10271033
siteApiNamespace.append("sites/\(siteId)/")
10281034
}

WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationCommentDetailViewController.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,6 @@ private extension NotificationCommentDetailViewController {
185185
}
186186

187187
self.fetchParentCommentIfNeeded(completion: {
188-
if let comment = self.loadCommentFromCache(self.commentID) {
189-
self.comment = comment
190-
return
191-
}
192188
self.fetchComment(self.commentID, completion: { comment in
193189
guard let comment else {
194190
self.showErrorView(title: NoResults.errorTitle, subtitle: NoResults.errorSubtitle)

0 commit comments

Comments
 (0)