-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Allow XML-RPC to be disabled on self-hosted sites #25183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1abfab0
4bdd0d4
d7247fa
7d8de35
8f8b2df
7eb019b
e501a98
fd789fc
71bdc79
1a7fde2
df6ace3
4f2a9a4
76942f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,6 +114,27 @@ 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 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 | ||
| } | ||
| return true | ||
|
||
| } | ||
|
|
||
| /** | ||
| Executes a XMLRPC call for the method specificied with the arguments provided. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,23 @@ struct SelfHostedSiteAuthenticator { | |
| throw .mismatchedUser(expectedUsername: username) | ||
| } | ||
|
|
||
| let blog = try await fetchSiteDataUsingCoreRESTAPI(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<Blog> { | ||
| 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 | ||
| } | ||
|
|
||
|
|
@@ -272,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<Blog> { | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how accurate this is, but I did the CC check for this: |
||
| // 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<Blog> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) should probably be named |
||
| 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 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) I'd suggest naming it
isXMLRPCEnabled()for clarity.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I omitted the "XMLRPC" part because the type is
WordPressOrgXMLRPCApi.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the logic behind omitting it, but IMHO it's a bit clearer if we re-state it. Alternatively, a good docblock would help here.