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

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

4 changes: 3 additions & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ let package = Package(
revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"
),
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.18.1"),
// TODO: Restore a version pin once GutenbergKit#569 ships a release.
.package(url: "https://github.com/wordpress-mobile/GutenbergKit",
revision: "9f3a5c203d0d81b4bdfe6851cd1bf3b7454e22c9"),
.package(
url: "https://github.com/automattic/wordpress-rs",
exact: "0.6.0"
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
27.2
-----
* [*] Experimental Gutenberg editor: fix block assets not loading on self-hosted sites that use plain permalinks [#25859]


27.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,72 @@ struct EditorConfigurationTests {
#expect(config.authHeader == "Basic \(base64Credentials)", "Should use Basic authentication")
#expect(config.siteApiNamespace.isEmpty, "Should not have WP.com API namespace")
}

// MARK: - Editor Assets Endpoint

@Test("Editor assets endpoint is appended to a path-based API root")
func editorAssetsEndpointWithPathBasedApiRoot() async throws {
let context = self.context

let blog = BlogBuilder(context)
.with(atomic: false)
.isNotHostedAtWPcom()
.with(username: "selfhosteduser")
.with(url: "https://self-hosted.org")
.withApplicationPassword("test-app-password-1234", using: keychain)
.with(restApiRootURL: "https://self-hosted.org/wp-json/")
.build()

let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain)

#expect(
config.editorAssetsEndpoint
== URL(string: "https://self-hosted.org/wp-json/wpcom/v2/editor-assets"),
"Should append the endpoint to the path of a path-based API root"
)
}

/// Sites using plain permalinks have no path-based REST root, so WordPress advertises the
/// query form and the endpoint belongs in the `rest_route` value rather than the URL path.
@Test("Editor assets endpoint is appended to the route of a query-based API root")
func editorAssetsEndpointWithQueryBasedApiRoot() async throws {
let context = self.context

let blog = BlogBuilder(context)
.with(atomic: false)
.isNotHostedAtWPcom()
.with(username: "selfhosteduser")
.with(url: "https://self-hosted.org")
.withApplicationPassword("test-app-password-1234", using: keychain)
.with(restApiRootURL: "https://self-hosted.org/?rest_route=/")
.build()

let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain)

#expect(
config.editorAssetsEndpoint
== URL(string: "https://self-hosted.org/?rest_route=/wpcom/v2/editor-assets"),
"Should append the endpoint to the rest_route value, not the URL path"
)
}

@Test("Editor assets endpoint includes the site namespace for WP.com sites")
func editorAssetsEndpointIncludesNamespace() async throws {
let context = self.context

let blog = BlogBuilder(context)
.with(atomic: false)
.isHostedAtWPcom()
.withAnAccount(username: "simpleuser", authToken: "simple-bearer-token")
.with(dotComID: 12345)
.build()

let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain)

#expect(
config.editorAssetsEndpoint
== URL(string: "https://public-api.wordpress.com/wpcom/v2/sites/12345/editor-assets"),
"Should insert the site namespace before the endpoint"
)
}
}
40 changes: 33 additions & 7 deletions WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,43 @@ extension EditorConfiguration {
.setNetworkFallbackMode(.automatic)

// Build editor assets endpoint
var editorAssetsEndpoint = siteApiRoot
editorAssetsEndpoint.appendPathComponent("wpcom/v2/")
if let namespace = siteApiNamespace.first {
editorAssetsEndpoint.appendPathComponent(namespace)
}
editorAssetsEndpoint.appendPathComponent("editor-assets")
builder = builder.setEditorAssetsEndpoint(editorAssetsEndpoint)
let namespacePath = siteApiNamespace.first.map { $0.hasSuffix("/") ? $0 : $0 + "/" } ?? ""
builder = builder.setEditorAssetsEndpoint(
Self.appendingRESTPath("wpcom/v2/\(namespacePath)editor-assets", to: siteApiRoot)
)

self = builder.build()
}

/// Appends a REST API path to a site's API root.
///
/// Sites using plain permalinks have no path-based REST root — WordPress advertises the
/// query form `https://example.com/?rest_route=/` instead — so the path belongs in the
/// `rest_route` value rather than the URL path:
///
/// ```
/// https://example.com/?rest_route=/ + wpcom/v2/editor-assets
/// -> https://example.com/?rest_route=/wpcom/v2/editor-assets
/// ```
///
/// Path-based roots keep the usual behavior. This mirrors `@wordpress/api-fetch`'s root URL
/// middleware and GutenbergKit's native URL builders, so every layer resolves the same
/// endpoints for a given site.
///
/// - Parameters:
/// - path: The REST path to append, without a leading slash.
/// - apiRoot: The site's REST API root.
/// - Returns: The endpoint URL, or `apiRoot` unchanged if the result isn't a valid URL.
static func appendingRESTPath(_ path: String, to apiRoot: URL) -> URL {
// Concatenate onto the root's full string rather than appending path components, so a
// query-based root grows its `rest_route` value instead of stranding it behind the path.
// One slash is kept between the two whether the root ends in `wp-json/`, `wp-json`,
// `?rest_route=/`, or `?rest_route=`.
let root = apiRoot.absoluteString
let separator = root.hasSuffix("/") ? "" : "/"
return URL(string: root + separator + path) ?? apiRoot
}

/// Returns true if the plugins should be enabled for the given blog.
/// This is used to determine if the editor should load third-party
/// plugins providing blocks.
Expand Down
28 changes: 26 additions & 2 deletions WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension GutenbergKit.EditorViewControllerDelegate {
}
}

private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String {
private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String? {
switch value {
case .showMore:
NSLocalizedString(
Expand Down Expand Up @@ -91,6 +91,24 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString)
value: "All",
comment: "Category name for section showing all patterns"
)
case .patternsCount(let count):
if count == 1 {
NSLocalizedString(
"editor.patterns.count.singular",
value: "1 pattern",
comment: "Singular label displaying the number of patterns in a category"
)
} else {
String(
format: NSLocalizedString(
"editor.patterns.count.plural",
value: "%1$d patterns",
comment:
"Plural label displaying the number of patterns in a category. %1$d is a placeholder for the number of patterns."
),
count
)
}
case .loadingEditor:
NSLocalizedString(
"editor.loading.title",
Expand Down Expand Up @@ -134,11 +152,17 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString)
value: "Dismiss",
comment: "Button title to dismiss the Lockdown Mode warning"
)
// Declining a key lets the editor render its own string, so strings added
// by a newer GutenbergKit appear in English rather than breaking this build.
// `@unknown` because a plain `default` warns that it will never execute
// while this switch happens to cover every case.
@unknown default:
nil
}
}

extension EditorLocalizableString {
var localized: String {
var localized: String? {
getLocalizedString(for: self)
}
}