From 1abfab01eb34c2b9c7d0e0cb3a439a9f370e3890 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 12:25:34 +1300 Subject: [PATCH 01/11] Extract fetching site options via XMLRPC into a function --- .../Login/SelfHostedSiteAuthenticator.swift | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 228c2f1b4965..6699a1eb6121 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -192,6 +192,23 @@ struct SelfHostedSiteAuthenticator { throw .mismatchedUser(expectedUsername: username) } + let blog = try await fetchSiteDataUsingXMLRPC(credentials: credentials, apiRootURL: apiRootURL, context: context) + + switch context { + case .default: + NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) + case .reauthentication: + NotificationCenter.default.post(name: Self.applicationPasswordUpdated, object: nil) + } + + return blog + } + + private func fetchSiteDataUsingXMLRPC( + credentials: WpApiApplicationPasswordDetails, + apiRootURL: URL, + context: SignInContext + ) async throws(SignInError) -> TaggedManagedObjectID { let xmlrpc: URL = try await discoverXMLRPCEndpoint(site: credentials.siteUrl) let blogOptions: [AnyHashable: Any] do { @@ -235,13 +252,6 @@ struct SelfHostedSiteAuthenticator { } } - switch context { - case .default: - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) - case .reauthentication: - NotificationCenter.default.post(name: Self.applicationPasswordUpdated, object: nil) - } - return blog } From 4bdd0d496f8823f4161131d588876e69d1c73f13 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 14:04:42 +1300 Subject: [PATCH 02/11] Allow XML-RPC to be disabled on self-hosted sites --- .../Login/SelfHostedSiteAuthenticator.swift | 65 ++++++++++++++++++- .../Classes/Services/Facades/BlogSyncFacade.m | 1 + 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 6699a1eb6121..67f2ccb11cd0 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -192,7 +192,7 @@ struct SelfHostedSiteAuthenticator { throw .mismatchedUser(expectedUsername: username) } - let blog = try await fetchSiteDataUsingXMLRPC(credentials: credentials, apiRootURL: apiRootURL, context: context) + let blog = try await fetchSiteDataUsingCoreRESTAPI(credentials: credentials, apiRootURL: apiRootURL, context: context) switch context { case .default: @@ -282,4 +282,67 @@ struct SelfHostedSiteAuthenticator { } } + // This is an alternative to `fetchSiteDataUsingXMLRPC`, without requiring the site's XML-RPC to be enabled. + private func fetchSiteDataUsingCoreRESTAPI( + credentials: WpApiApplicationPasswordDetails, + apiRootURL: URL, + context: SignInContext + ) async throws(SignInError) -> TaggedManagedObjectID { + let api = WordPressAPI( + urlSession: URLSession(configuration: .ephemeral), + apiRootUrl: try! ParsedUrl.parse(input: apiRootURL.absoluteString), + authentication: WpAuthentication(username: credentials.userLogin, password: credentials.password) + ) + + let siteTitle: String + let isAdmin: Bool + do { + async let settingsResponse = api.siteSettings.retrieveWithViewContext() + async let userResponse = api.users.retrieveMeWithEditContext() + let (settings, user) = try await (settingsResponse.data, userResponse.data) + + isAdmin = user.roles.contains(.administrator) + siteTitle = settings.title + } catch { + throw .loadingSiteInfoFailure + } + + // If the XMLRPC is disabled, we'll use the default endpoint. + let xmlrpc = (try? await discoverXMLRPCEndpoint(site: credentials.siteUrl)) + ?? URL(string: credentials.siteUrl)?.appending(component: "xmlrpc.php") + guard let xmlrpc else { + throw .loadingSiteInfoFailure + } + + // FIXME: The XML-RPC version stores `wp.getOptions` result in `Blog.options`, which is used in a few places + // in the app. We can't get the same "options" via REST API. We'll need to investigate the impact of missing + // "options". + + let blog: TaggedManagedObjectID + do { + blog = try await Blog.createRestApiBlog( + with: credentials, + restApiRootURL: apiRootURL, + xmlrpcEndpointURL: xmlrpc, + blogID: context.blogID, + in: ContextManager.shared + ) + + try await ContextManager.shared.performAndSave { context in + let blog = try context.existingObject(with: blog) + + // Here we'll use the "application password" as the "account password". + blog.password = credentials.password + blog.isAdmin = isAdmin + blog.addSettingsIfNecessary() + blog.settings?.name = siteTitle + } + + try await ApplicationPasswordRepository.shared.saveApplicationPassword(of: blog) + } catch { + throw .savingSiteFailure + } + + return blog + } } diff --git a/WordPress/Classes/Services/Facades/BlogSyncFacade.m b/WordPress/Classes/Services/Facades/BlogSyncFacade.m index 9d1a5a29cd8c..03ec263beb73 100644 --- a/WordPress/Classes/Services/Facades/BlogSyncFacade.m +++ b/WordPress/Classes/Services/Facades/BlogSyncFacade.m @@ -44,6 +44,7 @@ - (void)syncBlogWithUsername:(NSString *)username blog.url = url; } if (blogName) { + [blog addSettingsIfNecessary]; blog.settings.name = [blogName stringByDecodingXMLCharacters]; } } From d7247faec0baa8c5afa408907e88b063908028ba Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 16:04:55 +1300 Subject: [PATCH 03/11] Show a "XMLRPC Disabled" card if disabled --- .../WordPressKit/WordPressOrgXMLRPCApi.swift | 10 ++ .../BlogDetailsTableViewModel.swift | 23 +++- .../BlogDetailsViewController.swift | 22 ++++ .../Blog Details/XMLRPCDisabledCell.swift | 104 ++++++++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift diff --git a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift index b57c1fc73ae6..4f1359c99011 100644 --- a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift +++ b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift @@ -114,6 +114,16 @@ open class WordPressOrgXMLRPCApi: NSObject, WordPressOrgXMLRPCApiInterfacing { let parameters: [AnyObject] = [0 as AnyObject, username as AnyObject, password as AnyObject] callMethod("wp.getOptions", parameters: parameters, success: success, failure: failure) } + + public func isEnabled(username: String, password: String) async -> Bool { + let parameters: [AnyObject] = [0 as AnyObject, username as AnyObject, password as AnyObject] + let result = await call(method: "wp.getOptions", parameters: parameters) + guard case let .failure(error) = result, case let .endpointError(fault) = error else { + return true + } + return fault.code != 405 + } + /** Executes a XMLRPC call for the method specificied with the arguments provided. diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift index 5457d4e4383c..903fd89f9078 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift @@ -78,6 +78,7 @@ private struct Section { tableView.register(JetpackBrandingMenuCardCell.self, forCellReuseIdentifier: CellIdentifiers.jetpackBrandingCard) tableView.register(JetpackRemoteInstallTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.jetpackInstall) tableView.register(ExtensiveLoggingCell.self, forCellReuseIdentifier: CellIdentifiers.extensiveLogging) + tableView.register(XMLRPCDisabledCell.self, forCellReuseIdentifier: CellIdentifiers.xmlrpcDisabled) tableView.delegate = self tableView.dataSource = self @@ -106,6 +107,10 @@ private struct Section { newSections.append(Section(rows: [], category: .extensiveLogging)) } + if viewController.showXMLRPCDisabled { + newSections.append(Section(rows: [], category: .xmlrpcDisabled)) + } + if viewController.isDashboardEnabled() && isSplitViewDisplayed { newSections.append(buildHomeSection()) } @@ -243,7 +248,7 @@ extension BlogDetailsTableViewModel: UITableViewDataSource { guard section < sections.count else { return 0 } switch sections[section].category { - case .jetpackInstallCard, .migrationSuccess, .jetpackBrandingCard, .extensiveLogging: + case .jetpackInstallCard, .migrationSuccess, .jetpackBrandingCard, .extensiveLogging, .xmlrpcDisabled: // The "card" sections do not set the `rows` property. It's hard-coded to show specific types of cards. wpAssert(sections[section].rows.count == 0) return 1 @@ -269,6 +274,8 @@ extension BlogDetailsTableViewModel: UITableViewDataSource { cell = configureJetpackBrandingCell(tableView: tableView) case .extensiveLogging: cell = configureExtensiveLoggingCell(tableView: tableView) + case .xmlrpcDisabled: + cell = configureXMLRPCDisabledCell(tableView: tableView) default: if indexPath.row < section.rows.count { let row = section.rows[indexPath.row] @@ -496,6 +503,18 @@ private extension BlogDetailsTableViewModel { cell.configure(with: viewController) return cell } + + func configureXMLRPCDisabledCell(tableView: UITableView) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CellIdentifiers.xmlrpcDisabled + ) as? XMLRPCDisabledCell, + let viewController else { + return UITableViewCell() + } + + cell.configure(with: viewController) + return cell + } } private extension BlogDetailsTableViewModel { @@ -827,6 +846,7 @@ private enum SectionCategory { case reminders case domainCredit case extensiveLogging + case xmlrpcDisabled case home case general case jetpack @@ -1493,4 +1513,5 @@ private enum CellIdentifiers { static let jetpackBrandingCard = "BlogDetailsJetpackBrandingCardCellIdentifier" static let jetpackInstall = "BlogDetailsJetpackInstallCardCellIdentifier" static let extensiveLogging = "BlogDetailsExtensiveLoggingCellIdentifier" + static let xmlrpcDisabled = "BlogDetailsXMLRPCDisabledCellIdentifier" } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift index 8d60c775c0e7..4cf7bdc07c3f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift @@ -1,5 +1,6 @@ import UIKit import WordPressData +import WordPressKit import WordPressShared import WordPressUI import Reachability @@ -21,6 +22,7 @@ public class BlogDetailsViewController: UIViewController { private lazy var blogService = BlogService(coreDataStack: ContextManager.shared) private var hasLoggedDomainCreditPromptShownEvent = false + private(set) var showXMLRPCDisabled: Bool = false init(blog: Blog) { self.blog = blog @@ -80,6 +82,7 @@ public class BlogDetailsViewController: UIViewController { observeManagedObjectContextObjectsDidChangeNotification() observeGravatarImageUpdate() downloadGravatarImage() + checkXMLRPCStatus() registerForTraitChanges([UITraitHorizontalSizeClass.self], action: #selector(handleTraitChanges)) } @@ -192,6 +195,24 @@ public class BlogDetailsViewController: UIViewController { blogService.refreshDomains(for: blog, success: nil, failure: nil) } + private func checkXMLRPCStatus() { + guard let xmlrpcApi = blog.xmlrpcApi, let username = blog.username, let password = blog.password else { + showXMLRPCDisabled = false + return + } + + Task { @MainActor in + let isEnabled = await xmlrpcApi.isEnabled(username: username, password: password) + let wasDisabled = self.showXMLRPCDisabled + self.showXMLRPCDisabled = !isEnabled + + if wasDisabled != self.showXMLRPCDisabled { + self.configureTableViewData() + self.reloadTableViewPreservingSelection() + } + } + } + public func showRemoveSiteAlert() { let model = UIDevice.current.localizedModel let message = String(format: NSLocalizedString( @@ -243,6 +264,7 @@ public class BlogDetailsViewController: UIViewController { @objc private func handleWillEnterForeground(_ notification: NSNotification) { configureTableViewData() reloadTableViewPreservingSelection() + checkXMLRPCStatus() } private func observeManagedObjectContextObjectsDidChangeNotification() { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift new file mode 100644 index 000000000000..952e1813835e --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift @@ -0,0 +1,104 @@ +import UIKit +import DesignSystem +import SwiftUI +import WordPressUI + +class XMLRPCDisabledCell: UITableViewCell { + private weak var presenterViewController: UIViewController? + + private lazy var cardView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .secondarySystemGroupedBackground + view.layer.masksToBounds = true + view.layer.cornerRadius = DesignConstants.radius(.large) + + let content = UIHostingView(view: CardContent()) + content.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(content) + view.pinSubviewToAllEdges(content) + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showAlert))) + return view + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + contentView.addSubview(cardView) + contentView.pinSubviewToAllEdges(cardView) + } + + func configure(with viewController: BlogDetailsViewController) { + presenterViewController = viewController + } + + @objc private func showAlert() { + // TODO: Change to a modal, with more information (i.e. show related plugins) + let alert = UIAlertController( + title: Strings.alertTitle, + message: Strings.alertMessage, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default)) + presenterViewController?.present(alert, animated: true) + } +} + +private struct CardContent: View { + var body: some View { + HStack(spacing: 8) { + Circle() + .fill(.orange) + .frame(width: 8, height: 8) + + VStack(alignment: .leading) { + Text(Strings.cardTitle) + .font(.subheadline) + .fontWeight(.medium) + Text(Strings.cardSubtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "info.circle.fill") + .foregroundStyle(.tertiary) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + } +} + +private enum Strings { + static let cardTitle = NSLocalizedString( + "blogDetails.xmlrpcDisabled.card.title", + value: "XML-RPC Disabled", + comment: "Title for the XML-RPC disabled card on blog details" + ) + static let cardSubtitle = NSLocalizedString( + "blogDetails.xmlrpcDisabled.card.subtitle", + value: "Some features may be limited", + comment: "Subtitle for the XML-RPC disabled card on blog details" + ) + + static let alertTitle = NSLocalizedString( + "blogDetails.xmlrpcDisabled.alert.title", + value: "XML-RPC Disabled", + comment: "Alert title for XML-RPC disabled" + ) + + static let alertMessage = NSLocalizedString( + "blogDetails.xmlrpcDisabled.alert.message", + value: "XML-RPC is currently unavailable on your site. The app is transitioning to WordPress REST API, but some features still require XML-RPC. You may experience limited functionality until this transition is complete.", + comment: "Alert message explaining that XML-RPC is disabled on the site and some features may be limited" + ) +} From 7d8de35f679e6bfb4f75701b24d4af316195b23f Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 18:00:24 +1300 Subject: [PATCH 04/11] Hide activity indicator in the "Posts" screen upon fetching failure --- .../Post/Controllers/AbstractPostListViewController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift index 952684cfd3a5..8fc54e70d206 100644 --- a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift @@ -452,8 +452,9 @@ class AbstractPostListViewController: UIViewController, return } - // Update in the background - syncItemsWithUserInteraction(false) + // If it's in the middle of pushing this controller, we'll treat it as "syncing with user interaction". + let userInteraction = isMovingToParent + syncItemsWithUserInteraction(userInteraction) } @objc func syncItemsWithUserInteraction(_ userInteraction: Bool) { @@ -608,6 +609,7 @@ class AbstractPostListViewController: UIViewController, hideRefreshingIndicator() dismissAllNetworkErrorNotices() + refreshResults() // If there is no internet connection, we'll show the specific error message defined in // `noConnectionMessage()` (overridden by subclasses). For everything else, we let From 8f8b2df8be7feb5e5ec5c82c0d767add59da722e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 18:04:56 +1300 Subject: [PATCH 05/11] Enable self-hosted site user management feature flag --- WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 5e808b3478bf..d4f6c2e97977 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -71,7 +71,7 @@ public enum FeatureFlag: Int, CaseIterable { case .allowApplicationPasswords: return false case .selfHostedSiteUserManagement: - return false + return true case .readerGutenbergCommentComposer: return false case .pluginManagementOverhaul: From 7eb019b3259ab7f900a4eb0300719ef6f8b0c382 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 30 Jan 2026 18:20:33 +1300 Subject: [PATCH 06/11] Add a release note --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e3ca391f9ada..cb1378d85a6c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,7 @@ 26.7 ----- * [**] Stats: Add new "Adds" tab to show WordAdds earnings and stats [#25165] +* [*] For sites that are authenticated with application passwords, the app no longer requires xml-rpc to be enabled. But in that scenario, only limited features are available: XML-RPC needs to be enabled to use all features. [#25183] 26.6 ----- From e501a98598dab7c596abaac265eb26c820995726 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 2 Feb 2026 17:42:14 +1300 Subject: [PATCH 07/11] Add more checks to check if XML-RPC is enabled --- .../WordPressKit/WordPressOrgXMLRPCApi.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift index 4f1359c99011..8c48a65fb46a 100644 --- a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift +++ b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift @@ -118,10 +118,21 @@ open class WordPressOrgXMLRPCApi: NSObject, WordPressOrgXMLRPCApiInterfacing { public func isEnabled(username: String, password: String) async -> Bool { let parameters: [AnyObject] = [0 as AnyObject, username as AnyObject, password as AnyObject] let result = await call(method: "wp.getOptions", parameters: parameters) - guard case let .failure(error) = result, case let .endpointError(fault) = error else { - return true + guard case let .failure(error) = result else { return true } + // 405 is a proper fault code that indicates XML-RPC is disabled. + if case let .endpointError(fault) = error, fault.code == 405 { + return false } - return fault.code != 405 + // Some plugins send HTTP 403 Forbidden response. + if error.response?.statusCode == 403 { + return false + } + // Some plugins send HTTP 200 with html or no content at all. + if case let .unparsableResponse(response, _, _) = error, + response?.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("text/xml") == false { + return false + } + return true } /** From fd789fc058236744e78fb0461d21a95932675c85 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 3 Feb 2026 11:38:23 +1300 Subject: [PATCH 08/11] Do not store application passwords as account passwords --- Sources/WordPressData/Objective-C/Blog.m | 8 +++++++- Sources/WordPressData/Swift/Blog+SelfHosted.swift | 5 +++++ WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift | 2 -- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/WordPressData/Objective-C/Blog.m b/Sources/WordPressData/Objective-C/Blog.m index 2416fc07bb89..58db0042dc1b 100644 --- a/Sources/WordPressData/Objective-C/Blog.m +++ b/Sources/WordPressData/Objective-C/Blog.m @@ -444,7 +444,13 @@ - (NSString *)version - (NSString *)password { - return [SFHFKeychainUtils getPasswordForUsername:self.username andServiceName:self.xmlrpc accessGroup:nil error:nil]; + NSString *accountPassword = [SFHFKeychainUtils getPasswordForUsername:self.username andServiceName:self.xmlrpc accessGroup:nil error:nil]; + if (accountPassword != nil) { + return accountPassword; + } + + // Application password can also be used to authenticate XML-RPC. + return [self getApplicationTokenWithError:nil]; } - (void)setPassword:(NSString *)password diff --git a/Sources/WordPressData/Swift/Blog+SelfHosted.swift b/Sources/WordPressData/Swift/Blog+SelfHosted.swift index c7122c85750c..15cd91f5684a 100644 --- a/Sources/WordPressData/Swift/Blog+SelfHosted.swift +++ b/Sources/WordPressData/Swift/Blog+SelfHosted.swift @@ -55,6 +55,11 @@ public extension Blog { BlogQuery().apiKey(is: id).count(in: context) != 0 } + @objc(getApplicationTokenWithError:) + func objc_getApplicationToken() throws -> String { + try getApplicationToken() + } + // MARK: Type-safe wrappers // The underlying `Blog` object has lots of field nullability that doesn't provide guarantees about // which fields are present. These wrappers will `throw` if the `Blog` is invalid, allowing any dependent diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 67f2ccb11cd0..23e651a3b78c 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -331,8 +331,6 @@ struct SelfHostedSiteAuthenticator { try await ContextManager.shared.performAndSave { context in let blog = try context.existingObject(with: blog) - // Here we'll use the "application password" as the "account password". - blog.password = credentials.password blog.isAdmin = isAdmin blog.addSettingsIfNecessary() blog.settings?.name = siteTitle From 71bdc799e3b46208a7cef88ad9395d78f7576699 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 3 Feb 2026 11:43:50 +1300 Subject: [PATCH 09/11] Only show the XML-RPC disabled card if the site used as a self-hosted site --- .../Blog/Blog Details/BlogDetailsTableViewModel.swift | 2 +- .../Blog/Blog Details/BlogDetailsViewController.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift index 903fd89f9078..004ce9e18aa2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift @@ -107,7 +107,7 @@ private struct Section { newSections.append(Section(rows: [], category: .extensiveLogging)) } - if viewController.showXMLRPCDisabled { + if blog.isSelfHosted, viewController.showXMLRPCDisabled { newSections.append(Section(rows: [], category: .xmlrpcDisabled)) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift index 4cf7bdc07c3f..0d77d7986d47 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift @@ -196,7 +196,8 @@ public class BlogDetailsViewController: UIViewController { } private func checkXMLRPCStatus() { - guard let xmlrpcApi = blog.xmlrpcApi, let username = blog.username, let password = blog.password else { + guard blog.isSelfHosted, let xmlrpcApi = blog.xmlrpcApi, + let username = blog.username, let password = blog.password else { showXMLRPCDisabled = false return } From 1a7fde2dad849ca2df720e4c6b8f1cb6b2f9f348 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 3 Feb 2026 13:03:36 +1300 Subject: [PATCH 10/11] Handle error separately to improve code readability --- .../WordPressKit/WordPressOrgXMLRPCApi.swift | 55 ++++++++++++++----- .../BlogDetailsViewController.swift | 4 +- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift index 8c48a65fb46a..b6473577b0c2 100644 --- a/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift +++ b/Modules/Sources/WordPressKit/WordPressOrgXMLRPCApi.swift @@ -115,24 +115,43 @@ open class WordPressOrgXMLRPCApi: NSObject, WordPressOrgXMLRPCApiInterfacing { callMethod("wp.getOptions", parameters: parameters, success: success, failure: failure) } - public func isEnabled(username: String, password: String) async -> Bool { + public func isXMLRPCAvailable(username: String, password: String) async -> XMLRPCAvailability { let parameters: [AnyObject] = [0 as AnyObject, username as AnyObject, password as AnyObject] let result = await call(method: "wp.getOptions", parameters: parameters) - guard case let .failure(error) = result else { return true } - // 405 is a proper fault code that indicates XML-RPC is disabled. - if case let .endpointError(fault) = error, fault.code == 405 { - return false - } - // Some plugins send HTTP 403 Forbidden response. - if error.response?.statusCode == 403 { - return false - } - // Some plugins send HTTP 200 with html or no content at all. - if case let .unparsableResponse(response, _, _) = error, - response?.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("text/xml") == false { - return false + guard case let .failure(error) = result else { return .available } + + switch error { + // This is the most ideal error case, where the site sent an HTTP 200 response with an "fault" XML. + case let .endpointError(fault): + // 405 is a proper fault code that indicates XML-RPC is disabled. + return fault.code == 405 ? .unavailable : .available + + // This error means the site sends an non-200 status code, which can mean anything. + case let .unacceptableStatusCode(response, _): + if response.statusCode == 404 { + return .unavailable + } + + // If the response is not an XML, we'll treat it as disabled. Some plugin does this. + if response.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("text/xml") == false { + return .unavailable + } + + return .unknown + + // The site returned an HTTP 200 with an response that we can't parse (which is likely not xml). + case let .unparsableResponse(response, _, _): + if response?.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("text/xml") == false { + return .unavailable + } + return .unknown + + // Treat the following errors as unknown, because we don't know for certain in these cases. + // The `connection` error (failing to send the request or receive the response) is mostly likely + // to be the only possible case here. + case .connection, .requestEncodingFailure, .unknown: + return .unknown } - return true } /** @@ -458,3 +477,9 @@ private extension WordPressAPIError where EndpointError == WordPressOrgXMLRPCApi } } + +public enum XMLRPCAvailability: Equatable { + case available + case unavailable + case unknown +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift index 0d77d7986d47..f2facff4a707 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift @@ -203,9 +203,9 @@ public class BlogDetailsViewController: UIViewController { } Task { @MainActor in - let isEnabled = await xmlrpcApi.isEnabled(username: username, password: password) + let availability = await xmlrpcApi.isXMLRPCAvailable(username: username, password: password) let wasDisabled = self.showXMLRPCDisabled - self.showXMLRPCDisabled = !isEnabled + self.showXMLRPCDisabled = availability == .unavailable if wasDisabled != self.showXMLRPCDisabled { self.configureTableViewData() From df6ace3e141291d2747b4648a65c348cdb6cfa73 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 3 Feb 2026 13:10:35 +1300 Subject: [PATCH 11/11] Use 'AlertView' instead of the OS alert --- .../Blog Details/XMLRPCDisabledCell.swift | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift index 952e1813835e..9d7ffc29cdfd 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/XMLRPCDisabledCell.swift @@ -41,14 +41,21 @@ class XMLRPCDisabledCell: UITableViewCell { } @objc private func showAlert() { - // TODO: Change to a modal, with more information (i.e. show related plugins) - let alert = UIAlertController( - title: Strings.alertTitle, - message: Strings.alertMessage, - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default)) - presenterViewController?.present(alert, animated: true) + guard let presenter = presenterViewController else { + return + } + + let alert = AlertView { + AlertHeaderView(title: Strings.alertTitle, description: Strings.alertMessage) + } content: { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 50)) + .foregroundStyle(.orange) + } actions: { + AlertDismissButton() + } + + alert.present(in: presenter) } }