diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 5e4613747529..ac466d6faccf 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "394a1dae231289152fc0a7f94cfb495587468c99f146311d7a94ac6a25772e0b", + "originHash" : "f31432f3579f8c471514d5d994fcbc2a2e5c8dc5d2ca211288c0f9c622bb1da2", "pins" : [ { "identity" : "alamofire", @@ -131,8 +131,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "7180587f49d3c3bfdb34cc3e80b2a9d22a3cd93e", - "version" : "0.18.1" + "revision" : "9f3a5c203d0d81b4bdfe6851cd1bf3b7454e22c9" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 9d07d6b80bb7..b9bebe6a0fcd 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -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" diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 718fbe84fd7c..407e1664a60c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -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 diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift index 9619becc4cab..92f3ad25133a 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift @@ -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" + ) + } } diff --git a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift index dd53bcaa557d..2036dce078b1 100644 --- a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift +++ b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift @@ -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. diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index cb89424b2db9..0ac42da4fb9f 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -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( @@ -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", @@ -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) } }