From 069f8aa189817a53c02d2c1284b41da9b850d2b4 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 11 Jul 2025 09:12:52 -0400 Subject: [PATCH 1/4] feat: Authenticate WebView requests for private sites Ensure WebView requests for private resources succeed. --- .../NewGutenbergViewController.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index da80ad8a2d96..ff835927659d 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -877,6 +877,24 @@ extension EditorConfiguration { self.namespaceExcludedPaths = ["/wpcom/v2/following/recommendations", "/wpcom/v2/following/mine"] self.authHeader = authHeader + if blog.isPrivate() && blog.isHostedAtWPcom { + if let blogURL = URL(string: blog.url ?? ""), + let cookies = HTTPCookieStorage.shared.cookies(for: blogURL) { + if let authCookie = cookies.first(where: { cookie in + cookie.name.hasPrefix("wordpress_logged_in") + }) { + let cookie = HTTPCookie(properties: [ + .domain: siteDomain, + .path: "/", + .name: authCookie.name, + .value: authCookie.value, + .secure: true + ])! + self.cookies.append(cookie) + } + } + } + self.themeStyles = FeatureFlag.newGutenbergThemeStyles.enabled // Limited to Simple sites until application password auth is supported if RemoteFeatureFlag.newGutenbergPlugins.enabled() && blog.isHostedAtWPcom { From 26ba6b0fc97c3967502a47fea1c088b108e5e49b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 11 Jul 2025 09:53:11 -0400 Subject: [PATCH 2/4] test: Assert GutenbergKit authentication cookie configuration --- .../Gutenberg/EditorConfigurationTests.swift | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift new file mode 100644 index 000000000000..3b880409e6d8 --- /dev/null +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift @@ -0,0 +1,215 @@ +import XCTest +import CoreData +import GutenbergKit +@testable import WordPress +@testable import WordPressData + +class EditorConfigurationTests: XCTestCase { + var context: NSManagedObjectContext! + + override func setUp() { + super.setUp() + context = ContextManager.forTesting().mainContext + } + + override func tearDown() { + // Clean up any cookies we added during testing + HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) + context = nil + super.tearDown() + } + + // MARK: - WordPress.com Private Site Cookie Tests + + func testEditorConfigurationIncludesCookieForPrivateWPComSiteWhenCookieExists() { + // Given: A private WordPress.com site + let blog = createPrivateWordPressComBlog() + let expectedCookieValue = "testuser%1234567890abcdef" + let blogHost = URL(string: blog.url ?? "")?.host ?? blog.primaryDomainAddress + + addWordPressLoggedInCookieToSystem(value: expectedCookieValue, domain: blogHost) + + // When: Creating an EditorConfiguration using the actual implementation + let configuration = EditorConfiguration(blog: blog) + + // Then: The configuration should include the wordpress_logged_in cookie + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 1, "Should include wordpress_logged_in cookie for private WP.com sites when cookie exists") + + if let cookie = wordPressCookies.first { + XCTAssertEqual(cookie.value, expectedCookieValue, "Cookie value should match the system cookie value") + XCTAssertEqual(cookie.domain, blog.primaryDomainAddress, "Cookie domain should match the site domain") + XCTAssertTrue(cookie.isSecure, "Cookie should be secure") + XCTAssertEqual(cookie.path, "/", "Cookie path should be root") + } + } + + func testEditorConfigurationDoesNotIncludeCookieForPrivateWPComSiteWhenNoCookieExists() { + // Given: A private WordPress.com site with no existing cookie + let blog = createPrivateWordPressComBlog() + // Ensure no wordpress_logged_in cookies exist + removeAllWordPressLoggedInCookies() + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: The configuration should not include any wordpress_logged_in cookies + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 0, "Should not include wordpress_logged_in cookie when none exists in system storage") + } + + func testEditorConfigurationDoesNotIncludeCookieForPublicWPComSite() { + // Given: A public WordPress.com site with an existing cookie + let blog = createPublicWordPressComBlog() + let domain = URL(string: blog.url ?? "")?.host ?? blog.primaryDomainAddress + + addWordPressLoggedInCookieToSystem(value: "testuser%1234567890abcdef", domain: domain) + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: The configuration should not include wordpress_logged_in cookies for public sites + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 0, "Should not include wordpress_logged_in cookie for public WP.com sites") + } + + func testEditorConfigurationDoesNotIncludeCookieForSelfHostedSite() { + // Given: A self-hosted site (even if private) + let blog = createSelfHostedBlog() + let domain = URL(string: blog.url ?? "")?.host ?? blog.primaryDomainAddress + + addWordPressLoggedInCookieToSystem(value: "testuser%1234567890abcdef", domain: domain) + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: The configuration should not include wordpress_logged_in cookies for self-hosted sites + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 0, "Should not include wordpress_logged_in cookie for self-hosted sites") + } + + func testEditorConfigurationIncludesCorrectCookieFromMultipleCandidates() { + // Given: A private WordPress.com site with multiple wordpress_logged_in cookies + let blog = createPrivateWordPressComBlog() + let primaryCookieValue = "testuser%primary123" + let secondaryCookieValue = "testuser%secondary456" + let domain = (URL(string: blog.url ?? "")?.host) ?? blog.primaryDomainAddress + + // Add multiple cookies with different names + addWordPressLoggedInCookieToSystem(name: "wordpress_logged_in", value: primaryCookieValue, domain: domain) + addWordPressLoggedInCookieToSystem(name: "wordpress_logged_in_abc123", value: secondaryCookieValue, domain: domain) + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: Should include exactly one cookie (the first one found) + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 1, "Should include exactly one wordpress_logged_in cookie") + + if let cookie = wordPressCookies.first { + // Should be one of the expected values (implementation determines which one is selected first) + XCTAssertTrue([primaryCookieValue, secondaryCookieValue].contains(cookie.value), + "Cookie value should be one of the cookies from system storage") + } + } + + func testEditorConfigurationHandlesAtomicSiteCookieFormat() { + // Given: A private Atomic WordPress.com site + let blog = createPrivateAtomicBlog() + let atomicCookieValue = "testuser|1544455667|KwKSrAKJsqIWCTtt2QImT3hFTgHuzDOaMprlWWZXQeQ|7f0a75827e7f72ce645ec817ac9a2ab58735e95752494494cc463d1ad5853add" + let domain = (URL(string: blog.url ?? "")?.host) ?? blog.primaryDomainAddress + + addWordPressLoggedInCookieToSystem(value: atomicCookieValue, domain: domain) + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: Should handle atomic cookie format correctly + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 1, "Should include wordpress_logged_in cookie for private Atomic sites") + + if let cookie = wordPressCookies.first { + XCTAssertEqual(cookie.value, atomicCookieValue, "Should preserve atomic cookie format") + XCTAssertTrue(cookie.value.contains("|"), "Atomic cookie should contain pipe separators") + } + } + + func testEditorConfigurationCookieNamePreservation() { + // Given: A private WordPress.com site with a hashed cookie name + let blog = createPrivateWordPressComBlog() + let hashedCookieName = "wordpress_logged_in_39d5e8179c238764ac288442f27d091b" + let cookieValue = "testuser%1234567890abcdef" + let domain = (URL(string: blog.url ?? "")?.host) ?? blog.primaryDomainAddress + + addWordPressLoggedInCookieToSystem(name: hashedCookieName, value: cookieValue, domain: domain) + + // When: Creating an EditorConfiguration + let configuration = EditorConfiguration(blog: blog) + + // Then: Should preserve the original cookie name + let wordPressCookies = configuration.cookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + XCTAssertEqual(wordPressCookies.count, 1, "Should include the hashed wordpress_logged_in cookie") + + if let cookie = wordPressCookies.first { + XCTAssertEqual(cookie.name, hashedCookieName, "Should preserve the original hashed cookie name") + XCTAssertEqual(cookie.value, cookieValue, "Should preserve the cookie value") + } + } + + // MARK: - Helper Methods + + private func createPrivateWordPressComBlog() -> Blog { + return BlogBuilder(context) + .with(isHostedAtWPCom: true) + .with(siteName: "Private Site") + .with(siteVisibility: .private) + .with(url: "https://privatesite.wordpress.com") + .build() + } + + private func createPublicWordPressComBlog() -> Blog { + return BlogBuilder(context) + .with(isHostedAtWPCom: true) + .with(siteName: "Public Site") + .with(siteVisibility: .public) + .with(url: "https://publicsite.wordpress.com") + .build() + } + + private func createSelfHostedBlog() -> Blog { + return BlogBuilder(context) + .with(isHostedAtWPCom: false) + .with(siteName: "Self Hosted Site") + .with(url: "https://selfhosted.example.com") + .build() + } + + private func createPrivateAtomicBlog() -> Blog { + return BlogBuilder(context) + .with(isHostedAtWPCom: true) + .with(siteName: "Atomic Site") + .with(atomic: true) + .with(siteVisibility: .private) + .with(url: "https://atomic.wordpress.com") + .build() + } + + private func addWordPressLoggedInCookieToSystem(name: String = "wordpress_logged_in", value: String, domain: String) { + let cookie = HTTPCookie(properties: [ + .domain: domain, + .path: "/", + .name: name, + .value: value, + .secure: true + ])! + HTTPCookieStorage.shared.setCookie(cookie) + } + + private func removeAllWordPressLoggedInCookies() { + let allCookies = HTTPCookieStorage.shared.cookies ?? [] + let wordPressCookies = allCookies.filter { $0.name.hasPrefix("wordpress_logged_in") } + for cookie in wordPressCookies { + HTTPCookieStorage.shared.deleteCookie(cookie) + } + } +} From f52d24be5bd2eb28c705bc58e2d5258e643bdb83 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 11 Jul 2025 12:01:28 -0400 Subject: [PATCH 3/4] build: Update GutenbergKit version --- Modules/Package.resolved | 5 ++--- Modules/Package.swift | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index a55dd61b0d48..0203524393de 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "729318612d504c093c0c0969353bb1df8e1a20bf7b46836e8885bb622196c02d", + "originHash" : "9100eabcb9758e16bf0e142233789fead26ec9d92b0b969bec5f166cbf96a12a", "pins" : [ { "identity" : "alamofire", @@ -149,8 +149,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "f7956aa46604fc08f14e120043befb14d1260137", - "version" : "0.4.1" + "revision" : "9cb742b38cd5dc1ea242ff943e44a6dfbf8d7d41" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 2d81c9f43459..3a371dfcf6f2 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -54,7 +54,8 @@ let package = Package( .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // We can't use wordpress-rs branches nor commits here. Only tags work. .package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250707"), - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.4.1"), + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", + revision: "9cb742b38cd5dc1ea242ff943e44a6dfbf8d7d41"), .package( url: "https://github.com/Automattic/color-studio", revision: "bf141adc75e2769eb469a3e095bdc93dc30be8de" From 353620de1e114eb8f6d0d40f1d1a6fce380a04f5 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 11 Jul 2025 12:01:36 -0400 Subject: [PATCH 4/4] feat: Remove unnecessary host check The private site feature is WPCOM-specific, so it implies the site is hosted at WPCOM. --- .../ViewRelated/NewGutenberg/NewGutenbergViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index ff835927659d..f14c9e7363f2 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -877,7 +877,7 @@ extension EditorConfiguration { self.namespaceExcludedPaths = ["/wpcom/v2/following/recommendations", "/wpcom/v2/following/mine"] self.authHeader = authHeader - if blog.isPrivate() && blog.isHostedAtWPcom { + if blog.isPrivate() { if let blogURL = URL(string: blog.url ?? ""), let cookies = HTTPCookieStorage.shared.cookies(for: blogURL) { if let authCookie = cookies.first(where: { cookie in