Skip to content

Commit 72dab72

Browse files
authored
Fixes fetching terms on XML-RPC disabled sites (#25767)
* Route tags and categories to the REST API on self-hosted sites Self-hosted taxonomy went through XML-RPC term methods, which some hosts block at the WAF, breaking tags and categories on new posts (#25758). Prefer the core REST API (wp/v2) whenever the site has application-password access, keeping XML-RPC only as a fallback for legacy sites without it. * Replace TagsViewModel convenience inits with named factory methods The two convenience initializers were ambiguous about when to use each. Introduce tags(for:) and taxonomy(_:client:) so call sites read by intent, and drop the unused blog parameter that was threaded through the custom taxonomy path. * Add release note * Format taxonomy Core REST service * Use view context for taxonomy reads * Fix a swiftlint issue
1 parent 98e73cd commit 72dab72

10 files changed

Lines changed: 126 additions & 36 deletions

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* [*] [internal] In-app updates: guard the flexible update notice against double-posting when two update checks race [#25666]
55
* [*] [build tooling] Add a String Catalog localization pipeline (plurals + build-free catalog generation) with a CI coverage gate [#25688]
66
* [*] [internal] Stats widgets: migrate the site picker from SiriKit to App Intents [#25753]
7+
* [*] Fix categories and tags not working on self-hosted sites with XML-RPC disabled [#25767]
78
* [*] Fix Blogging Reminders text color in dark mode [#25780]
89
* [*] Share extensions: Fix the site picker showing an error while sites are still loading [#25798]
910
* [*] Share Extension: Fix a crash after uploading a post to WordPress.com [#25773]

WordPress/Classes/Services/PostCategoryService.m

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,16 @@ - (void)mergeCategories:(NSArray <RemotePostCategory *> *)remoteCategories forBl
184184
if (blog.wordPressComRestApi) {
185185
return [[TaxonomyServiceRemoteREST alloc] initWithWordPressComRestApi:blog.wordPressComRestApi siteID:blog.dotComID];
186186
}
187-
} else if (blog.xmlrpcApi) {
188-
return [[TaxonomyServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password];
187+
} else {
188+
// Prefer the core REST API (wp/v2) for self-hosted sites. Some hosts
189+
// block the XML-RPC term methods, which breaks categories (#25758).
190+
TaxonomyServiceRemoteCoreREST *coreREST = [[TaxonomyServiceRemoteCoreREST alloc] initWithBlog:blog];
191+
if (coreREST) {
192+
return coreREST;
193+
}
194+
if (blog.xmlrpcApi) {
195+
return [[TaxonomyServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password];
196+
}
189197
}
190198
return nil;
191199
}

WordPress/Classes/Services/TaxonomyServiceRemoteCoreREST.swift

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import WordPressAPI
1717
self.client = client
1818
}
1919

20-
public func createCategory(_ category: RemotePostCategory, success: ((RemotePostCategory) -> Void)?, failure: ((any Error) -> Void)? = nil) {
20+
public func createCategory(
21+
_ category: RemotePostCategory,
22+
success: ((RemotePostCategory) -> Void)?,
23+
failure: ((any Error) -> Void)? = nil
24+
) {
2125
Task { @MainActor in
2226
do {
2327
let params = TermCreateParams(
@@ -33,10 +37,13 @@ import WordPressAPI
3337
}
3438
}
3539

36-
public func getCategoriesWithSuccess(_ success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) {
40+
public func getCategoriesWithSuccess(
41+
_ success: @escaping ([RemotePostCategory]) -> Void,
42+
failure: ((any Error) -> Void)? = nil
43+
) {
3744
Task { @MainActor in
3845
do {
39-
let sequence = await client.api.terms.sequenceWithEditContext(
46+
let sequence = await client.api.terms.sequenceWithViewContext(
4047
type: .categories,
4148
params: TermListParams(perPage: 100)
4249
)
@@ -51,7 +58,11 @@ import WordPressAPI
5158
}
5259
}
5360

54-
public func getCategoriesWith(_ paging: RemoteTaxonomyPaging, success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) {
61+
public func getCategoriesWith(
62+
_ paging: RemoteTaxonomyPaging,
63+
success: @escaping ([RemotePostCategory]) -> Void,
64+
failure: ((any Error) -> Void)? = nil
65+
) {
5566
Task { @MainActor in
5667
do {
5768
let params = TermListParams(
@@ -61,7 +72,7 @@ import WordPressAPI
6172
order: WpApiParamOrder(paging.order),
6273
orderby: WpApiParamTermsOrderBy(paging.orderBy)
6374
)
64-
let response = try await client.api.terms.listWithEditContext(
75+
let response = try await client.api.terms.listWithViewContext(
6576
termEndpointType: .categories,
6677
params: params
6778
)
@@ -73,11 +84,15 @@ import WordPressAPI
7384
}
7485
}
7586

76-
public func searchCategories(withName nameQuery: String, success: @escaping ([RemotePostCategory]) -> Void, failure: ((any Error) -> Void)? = nil) {
87+
public func searchCategories(
88+
withName nameQuery: String,
89+
success: @escaping ([RemotePostCategory]) -> Void,
90+
failure: ((any Error) -> Void)? = nil
91+
) {
7792
Task { @MainActor in
7893
do {
7994
let params = TermListParams(search: nameQuery)
80-
let response = try await client.api.terms.listWithEditContext(
95+
let response = try await client.api.terms.listWithViewContext(
8196
termEndpointType: .categories,
8297
params: params
8398
)
@@ -89,7 +104,11 @@ import WordPressAPI
89104
}
90105
}
91106

92-
public func createTag(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) {
107+
public func createTag(
108+
_ tag: RemotePostTag,
109+
success: ((RemotePostTag) -> Void)?,
110+
failure: ((any Error) -> Void)? = nil
111+
) {
93112
Task { @MainActor in
94113
do {
95114
let params = TermCreateParams(
@@ -109,7 +128,11 @@ import WordPressAPI
109128
}
110129
}
111130

112-
public func update(_ tag: RemotePostTag, success: ((RemotePostTag) -> Void)?, failure: ((any Error) -> Void)? = nil) {
131+
public func update(
132+
_ tag: RemotePostTag,
133+
success: ((RemotePostTag) -> Void)?,
134+
failure: ((any Error) -> Void)? = nil
135+
) {
113136
guard let tagID = tag.tagID else {
114137
failure?(URLError(.unknown))
115138
return
@@ -151,10 +174,13 @@ import WordPressAPI
151174
}
152175
}
153176

154-
public func getTagsWithSuccess(_ success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) {
177+
public func getTagsWithSuccess(
178+
_ success: @escaping ([RemotePostTag]) -> Void,
179+
failure: ((any Error) -> Void)? = nil
180+
) {
155181
Task { @MainActor in
156182
do {
157-
let response = try await client.api.terms.listWithEditContext(
183+
let response = try await client.api.terms.listWithViewContext(
158184
termEndpointType: .tags,
159185
params: TermListParams()
160186
)
@@ -166,7 +192,11 @@ import WordPressAPI
166192
}
167193
}
168194

169-
public func getTagsWith(_ paging: RemoteTaxonomyPaging, success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) {
195+
public func getTagsWith(
196+
_ paging: RemoteTaxonomyPaging,
197+
success: @escaping ([RemotePostTag]) -> Void,
198+
failure: ((any Error) -> Void)? = nil
199+
) {
170200
Task { @MainActor in
171201
do {
172202
let params = TermListParams(
@@ -176,7 +206,7 @@ import WordPressAPI
176206
order: WpApiParamOrder(paging.order),
177207
orderby: WpApiParamTermsOrderBy(paging.orderBy)
178208
)
179-
let response = try await client.api.terms.listWithEditContext(
209+
let response = try await client.api.terms.listWithViewContext(
180210
termEndpointType: .tags,
181211
params: params
182212
)
@@ -188,10 +218,14 @@ import WordPressAPI
188218
}
189219
}
190220

191-
public func searchTags(withName nameQuery: String, success: @escaping ([RemotePostTag]) -> Void, failure: ((any Error) -> Void)? = nil) {
221+
public func searchTags(
222+
withName nameQuery: String,
223+
success: @escaping ([RemotePostTag]) -> Void,
224+
failure: ((any Error) -> Void)? = nil
225+
) {
192226
Task { @MainActor in
193227
do {
194-
let response = try await client.api.terms.listWithEditContext(
228+
let response = try await client.api.terms.listWithViewContext(
195229
termEndpointType: .tags,
196230
params: TermListParams(search: nameQuery)
197231
)
@@ -205,6 +239,13 @@ import WordPressAPI
205239
}
206240

207241
private extension RemotePostCategory {
242+
convenience init(category: AnyTermWithViewContext) {
243+
self.init()
244+
self.categoryID = NSNumber(value: category.id)
245+
self.name = category.name
246+
self.parentID = NSNumber(value: category.parent ?? 0)
247+
}
248+
208249
convenience init(category: AnyTermWithEditContext) {
209250
self.init()
210251
self.categoryID = NSNumber(value: category.id)
@@ -214,6 +255,15 @@ private extension RemotePostCategory {
214255
}
215256

216257
private extension RemotePostTag {
258+
convenience init(tag: AnyTermWithViewContext) {
259+
self.init()
260+
self.tagID = NSNumber(value: tag.id)
261+
self.name = tag.name
262+
self.slug = tag.slug
263+
self.tagDescription = tag.description
264+
self.postCount = NSNumber(value: tag.count)
265+
}
266+
217267
convenience init(tag: AnyTermWithEditContext) {
218268
self.init()
219269
self.tagID = NSNumber(value: tag.id)

WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ extension SiteSettingsViewController {
6262
@objc public func showCustomTaxonomies() {
6363
let viewController: UIViewController
6464
if let client = try? WordPressClientFactory.shared.instance(for: .init(blog: blog)) {
65-
let rootView = SiteCustomTaxonomiesView(blog: self.blog, client: client)
65+
let rootView = SiteCustomTaxonomiesView(client: client)
6666
viewController = UIHostingController(rootView: rootView)
6767
} else {
6868
let feature = NSLocalizedString(
@@ -76,7 +76,7 @@ extension SiteSettingsViewController {
7676
source: "taxonomies",
7777
presentingViewController: self
7878
) { client in
79-
SiteCustomTaxonomiesView(blog: self.blog, client: client)
79+
SiteCustomTaxonomiesView(client: client)
8080
}
8181
viewController = UIHostingController(rootView: rootView)
8282
}

WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,6 @@ struct PostSettingsFormContentView<ViewModel: PostSettingsViewModelProtocol>: Vi
243243
ForEach(viewModel.customTaxonomies, id: \.slug) { taxonomy in
244244
NavigationLink {
245245
PostTagsView(
246-
blog: viewModel.blog,
247246
client: client,
248247
taxonomy: taxonomy,
249248
selectedTerms: viewModel.settings.getTerms(forTaxonomySlug: taxonomy.slug)

WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM
302302
Task { [weak self] in
303303
guard let self else { return }
304304

305-
let service = TagsService(blog: blog)
305+
let service = TagsViewModel.makeTagsService(for: blog)
306306
let resolved = await service.resolveTerms(named: pendingNames)
307307
for (name, existing) in resolved {
308308
if let index = settings.tags.firstIndex(where: { $0.name == name }) {

WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostTagsView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ struct PostTagsView: View {
1212
@State private var isKeyboardPresented = false
1313

1414
init(blog: Blog, selectedTags: [TagsViewModel.SelectedTerm], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) {
15-
let viewModel = TagsViewModel(blog: blog, selectedTags: selectedTags, mode: .selection(onSelectedTagsChanged: { tags in
15+
let viewModel = TagsViewModel.tags(for: blog, selectedTerms: selectedTags, mode: .selection(onSelectedTagsChanged: { tags in
1616
onSelectionChanged(tags)
1717
}))
1818
self._viewModel = StateObject(wrappedValue: viewModel)
1919
}
2020

21-
init(blog: Blog, client: WordPressClient, taxonomy: SiteTaxonomy, selectedTerms: [TagsViewModel.SelectedTerm] = [], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) {
22-
let viewModel = TagsViewModel(blog: blog, client: client, taxonomy: taxonomy, selectedTerms: selectedTerms, mode: .selection(onSelectedTagsChanged: { tags in
21+
init(client: WordPressClient, taxonomy: SiteTaxonomy, selectedTerms: [TagsViewModel.SelectedTerm] = [], onSelectionChanged: @escaping ([TagsViewModel.SelectedTerm]) -> Void) {
22+
let viewModel = TagsViewModel.taxonomy(taxonomy, client: client, selectedTerms: selectedTerms, mode: .selection(onSelectedTagsChanged: { tags in
2323
onSelectionChanged(tags)
2424
}))
2525
self._viewModel = StateObject(wrappedValue: viewModel)

WordPress/Classes/ViewRelated/Tags/SiteCustomTaxonomiesView.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ import WordPressShared
77
import WordPressUI
88

99
struct SiteCustomTaxonomiesView: View {
10-
let blog: Blog
1110
let client: WordPressClient
1211

1312
@State private var isLoading: Bool = false
1413
@State private var taxonomies: [SiteTaxonomy]? = nil
1514
@State private var error: Error?
1615

17-
init(blog: Blog, client: WordPressClient) {
18-
self.blog = blog
16+
init(client: WordPressClient) {
1917
self.client = client
2018
}
2119

2220
var body: some View {
2321
List {
2422
ForEach(taxonomies ?? [], id: \.slug) { taxonomy in
2523
NavigationLink {
26-
SiteTagsView(viewModel: .init(blog: blog, client: client, taxonomy: taxonomy, mode: .browse))
24+
SiteTagsView(viewModel: .taxonomy(taxonomy, client: client, mode: .browse))
2725
} label: {
2826
Text(taxonomy.localizedName)
2927
}

WordPress/Classes/ViewRelated/Tags/SiteTagsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ class SiteTagsViewController: UIHostingController<SiteTagsView> {
193193
let viewModel: TagsViewModel
194194

195195
init(blog: Blog) {
196-
viewModel = TagsViewModel(blog: blog, mode: .browse)
196+
viewModel = TagsViewModel.tags(for: blog, mode: .browse)
197197
super.init(rootView: .init(viewModel: viewModel))
198198
}
199199

WordPress/Classes/ViewRelated/Tags/TagsViewModel.swift

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,41 @@ class TagsViewModel: ObservableObject {
5757
return false
5858
}
5959

60-
convenience init(blog: Blog, selectedTags: [SelectedTerm] = [], mode: TagsViewMode) {
61-
self.init(taxonomy: nil, service: TagsService(blog: blog), selectedTerms: selectedTags, mode: mode)
60+
/// Builds a view model for a site's built-in tags. `makeTagsService(for:)`
61+
/// picks the backing service so self-hosted sites use REST where they can
62+
/// (issue #25758).
63+
static func tags(
64+
for blog: Blog,
65+
selectedTerms: [SelectedTerm] = [],
66+
mode: TagsViewMode
67+
) -> TagsViewModel {
68+
TagsViewModel(
69+
taxonomy: nil,
70+
service: makeTagsService(for: blog),
71+
selectedTerms: selectedTerms,
72+
mode: mode
73+
)
6274
}
6375

64-
convenience init(
65-
blog: Blog,
76+
/// Builds a view model for a specific site `taxonomy` (e.g. a custom
77+
/// taxonomy). Always uses the core REST API, which a custom taxonomy's
78+
/// `client` already guarantees is available.
79+
static func taxonomy(
80+
_ taxonomy: SiteTaxonomy,
6681
client: WordPressClient,
67-
taxonomy: SiteTaxonomy,
6882
selectedTerms: [SelectedTerm] = [],
6983
mode: TagsViewMode
70-
) {
71-
self.init(
84+
) -> TagsViewModel {
85+
TagsViewModel(
7286
taxonomy: taxonomy,
7387
service: AnyTermService(client: client, endpoint: taxonomy.endpoint),
7488
selectedTerms: selectedTerms,
7589
mode: mode
7690
)
7791
}
7892

93+
/// Designated initializer. Inject a `service` directly; production code
94+
/// should prefer the `tags(for:)` / `taxonomy(_:client:)` factories.
7995
init(
8096
taxonomy: SiteTaxonomy?,
8197
service: TaxonomyServiceProtocol,
@@ -90,6 +106,24 @@ class TagsViewModel: ObservableObject {
90106
self.selectedTagsSet = Set(selectedTerms.map { $0.name.lowercased() })
91107
}
92108

109+
/// Chooses the taxonomy service backing a blog's tags.
110+
///
111+
/// Sites with core REST API access (WordPress.com, or self-hosted sites with
112+
/// an application password) use `AnyTermService` so tags go through the wp/v2
113+
/// REST API. Some hosts block the XML-RPC term methods that `TagsService`
114+
/// uses for self-hosted sites, which breaks tags entirely (issue #25758).
115+
/// Legacy self-hosted sites without REST access keep using `TagsService`.
116+
static func makeTagsService(
117+
for blog: Blog,
118+
keychain: KeychainAccessible = AppKeychain()
119+
) -> TaxonomyServiceProtocol {
120+
if let site = try? WordPressSite(blog: blog, keychain: keychain) {
121+
let client = WordPressClientFactory.shared.instance(for: site)
122+
return AnyTermService(client: client, endpoint: .tags)
123+
}
124+
return TagsService(blog: blog)
125+
}
126+
93127
func onAppear() {
94128
guard response == nil else { return }
95129
Task {

0 commit comments

Comments
 (0)