Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -667,12 +667,8 @@ class GutenbergView : FrameLayout {
nativeUploadToken = uploadServer?.token
)
val gbKitJson = gbKit.toJsonString()
val gbKitConfig = """
window.GBKit = $gbKitJson;
localStorage.setItem('GBKit', JSON.stringify(window.GBKit));
""".trimIndent()

webView.evaluateJavascript(gbKitConfig, null)
webView.evaluateJavascript("window.GBKit = $gbKitJson;", null)
}

private fun startUploadServer() {
Expand Down Expand Up @@ -726,12 +722,7 @@ class GutenbergView : FrameLayout {
}

fun clearConfig() {
val jsCode = """
delete window.GBKit;
localStorage.removeItem('GBKit');
""".trimIndent()

webView.evaluateJavascript(jsCode, null)
webView.evaluateJavascript("delete window.GBKit;", null)
}

fun setContent(newContent: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ enum class CorsPolicy {
// `*` (any origin) rather than echoing a specific origin is
// deliberate, and safe here — not an oversight to tighten. The
// server is loopback-only, and every non-OPTIONS request is gated
// by a per-session random bearer token stored only in the editor
// origin's localStorage/window.GBKit, which is origin-scoped and
// unreadable by any other origin — so no cross-origin can obtain
// it. `*` only governs whether a *token-holding* origin may read
// by a per-session random bearer token held only in the editor
// origin's window.GBKit, which is origin-scoped and unreadable
// by any other origin — so no cross-origin can obtain it. `*`
// only governs whether a *token-holding* origin may read
// the response, and the sole token-holder is the editor itself, the
// legitimate client. Echoing the origin isn't viable anyway: the
// editor loads from file:// (Origin null), which can't be cleanly
Expand Down
23 changes: 17 additions & 6 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,26 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
nativeUploadToken: isUploadPipelineEnabled ? uploadServer?.token : nil,
networkProxy: networkProxyGlobal
)
let stringValue = try gbkitGlobal.toString()
return WKUserScript(
source: Self.configurationScript(gbkitGlobal: try gbkitGlobal.toString()),
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
}

let jsCode = """
window.GBKit = \(stringValue);
localStorage.setItem('GBKit', JSON.stringify(window.GBKit));
/// The document-start script that installs `window.GBKit`.
///
/// The configuration is session-scoped — it carries the site credential and
/// the local server's port and tokens — so no copy of it outlives the load
/// that injected it. Earlier versions mirrored it into `localStorage`, which
/// the default website data store keeps on disk across launches; the script
/// removes that key so a device upgraded from one of them is scrubbed.
static func configurationScript(gbkitGlobal: String) -> String {
"""
window.GBKit = \(gbkitGlobal);
localStorage.removeItem('GBKit');
"done";
"""

return WKUserScript(source: jsCode, injectionTime: .atDocumentStart, forMainFrameOnly: true)
}

/// Starts the local HTTP server for routing file uploads through native processing.
Expand Down
8 changes: 4 additions & 4 deletions ios/Sources/GutenbergKitHTTP/CORSPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public enum CORSPolicy: Sendable {
// `*` (any origin) rather than echoing a specific origin is
// deliberate, and safe here — not an oversight to tighten. The
// server is loopback-only, and every non-OPTIONS request is gated
// by a per-session random bearer token stored only in the editor
// origin's `localStorage`/`window.GBKit`, which is origin-scoped
// and unreadable by any other origin — so no cross-origin can
// obtain it. `*` only governs whether a *token-holding* origin may
// by a per-session random bearer token held only in the editor
// origin's `window.GBKit`, which is origin-scoped and unreadable
// by any other origin — so no cross-origin can obtain it. `*`
// only governs whether a *token-holding* origin may
// read the response, and the sole token-holder is the editor
// itself, the legitimate client. Echoing the origin isn't viable
// anyway: the editor loads from `file://` and WebKit sends
Expand Down
26 changes: 26 additions & 0 deletions ios/Tests/GutenbergKitTests/EditorConfigurationScriptTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import Testing

@testable import GutenbergKit

#if canImport(UIKit)

@Suite("Editor configuration script")
struct EditorConfigurationScriptTests {

@MainActor
@Test("injects the configuration without persisting it")
func doesNotPersistTheConfiguration() {
// The injected configuration carries the site credential and the local
// server's port and tokens, all of them valid only for this session.
let script = EditorViewController.configurationScript(
gbkitGlobal: #"{"authHeader":"Bearer secret"}"#
)

#expect(script.contains(#"window.GBKit = {"authHeader":"Bearer secret"};"#))
#expect(script.contains("localStorage.removeItem('GBKit')"))
#expect(!script.contains("localStorage.setItem"))
}
}

#endif
15 changes: 3 additions & 12 deletions src/utils/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,22 +261,13 @@ export const POST_FALLBACKS = {
};

/**
* Retrieves the native-host-provided GBKit object from localStorage or returns
* an empty object if not found.
* Retrieves the native-host-provided GBKit object or returns an empty object
* if the host has not injected one.
*
* @return {GBKitConfig} The GBKit object.
*/
export function getGBKit() {
if ( window.GBKit ) {
return window.GBKit;
}

try {
return JSON.parse( localStorage.getItem( 'GBKit' ) ) || {};
} catch ( err ) {
error( 'Failed to parse GBKit from localStorage', err );
return {};
}
return window.GBKit || {};
}

/**
Expand Down
Loading