Skip to content

Commit daf7305

Browse files
committed
impl + tests
1 parent b7f0b0c commit daf7305

File tree

9 files changed

+313
-63
lines changed

9 files changed

+313
-63
lines changed

android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorHTTPClient.kt

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package org.wordpress.gutenberg
22

33
import android.util.Log
4+
import com.google.gson.Gson
5+
import com.google.gson.JsonSyntaxException
6+
import com.google.gson.annotations.SerializedName
47
import kotlinx.coroutines.Dispatchers
58
import kotlinx.coroutines.withContext
69
import okhttp3.OkHttpClient
710
import okhttp3.Request
11+
import okhttp3.RequestBody.Companion.toRequestBody
812
import okhttp3.Response
913
import org.wordpress.gutenberg.model.http.EditorHTTPHeaders
1014
import java.io.File
@@ -124,10 +128,6 @@ class EditorHTTPClient(
124128
okHttpClient: OkHttpClient? = null
125129
) : EditorHTTPClientProtocol {
126130

127-
companion object {
128-
private const val TAG = "EditorHTTPClient"
129-
}
130-
131131
private val client: OkHttpClient = okHttpClient?.newBuilder()
132132
?.callTimeout(requestTimeoutSeconds, TimeUnit.SECONDS)
133133
?.build()
@@ -162,6 +162,7 @@ class EditorHTTPClient(
162162
input.copyTo(output)
163163
}
164164
}
165+
Log.d(TAG, "Downloaded file: file=${destination.absolutePath}, size=${destination.length()} bytes, url=$url")
165166
} ?: throw EditorHTTPClientError.DownloadFailed(statusCode)
166167

167168
EditorHTTPClientDownloadResponse(
@@ -173,10 +174,15 @@ class EditorHTTPClient(
173174

174175
override suspend fun perform(method: String, url: String): EditorHTTPClientResponse =
175176
withContext(Dispatchers.IO) {
177+
// OkHttp requires a body for POST, PUT, PATCH methods
178+
// GET, HEAD, OPTIONS, DELETE don't require a body
179+
val requiresBody = method.uppercase() in listOf("POST", "PUT", "PATCH")
180+
val requestBody = if (requiresBody) "".toRequestBody(null) else null
181+
176182
val request = Request.Builder()
177183
.url(url)
178184
.addHeader("Authorization", authHeader)
179-
.method(method, null)
185+
.method(method, requestBody)
180186
.build()
181187

182188
val response: Response
@@ -222,16 +228,32 @@ class EditorHTTPClient(
222228
private fun tryParseWPError(data: ByteArray): WPError? {
223229
return try {
224230
val json = data.toString(Charsets.UTF_8)
225-
val jsonObject = org.json.JSONObject(json)
226-
val code = jsonObject.optString("code", "")
227-
val message = jsonObject.optString("message", "")
228-
if (code.isNotEmpty() && message.isNotEmpty()) {
229-
WPError(code, message)
231+
val parsed = gson.fromJson(json, WPErrorJson::class.java)
232+
// Both code and message must be present (non-null) to be a valid WP error
233+
// Empty strings are accepted to match Swift behavior
234+
if (parsed.code != null && parsed.message != null) {
235+
WPError(parsed.code, parsed.message)
230236
} else {
231237
null
232238
}
239+
} catch (e: JsonSyntaxException) {
240+
null
233241
} catch (e: Exception) {
234242
null
235243
}
236244
}
245+
246+
/**
247+
* Internal data class for parsing WordPress error JSON responses.
248+
*/
249+
private data class WPErrorJson(
250+
@SerializedName("code") val code: String?,
251+
@SerializedName("message") val message: String?,
252+
@SerializedName("data") val data: Any? = null
253+
)
254+
255+
companion object {
256+
private const val TAG = "EditorHTTPClient"
257+
private val gson = Gson()
258+
}
237259
}

android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package org.wordpress.gutenberg
33
import android.annotation.SuppressLint
44
import android.content.Context
55
import android.content.Intent
6+
import android.graphics.Bitmap
67
import android.net.Uri
78
import android.os.Bundle
89
import android.os.Handler
910
import android.os.Looper
1011
import android.util.AttributeSet
1112
import android.util.Log
12-
import android.view.View
1313
import android.view.inputmethod.InputMethodManager
1414
import android.webkit.ConsoleMessage
1515
import android.webkit.CookieManager
@@ -29,6 +29,8 @@ import kotlinx.coroutines.withContext
2929
import org.json.JSONException
3030
import org.json.JSONObject
3131
import org.wordpress.gutenberg.model.EditorConfiguration
32+
import org.wordpress.gutenberg.model.EditorDependencies
33+
import org.wordpress.gutenberg.model.GBKitGlobal
3234
import org.wordpress.gutenberg.stores.EditorAssetsLibrary
3335
import java.io.File
3436
import java.net.URL
@@ -43,6 +45,7 @@ class GutenbergView : WebView {
4345
.addPathHandler("/assets/", AssetsPathHandler(this.context))
4446
.build()
4547
private var configuration: EditorConfiguration = EditorConfiguration.bundled()
48+
private var dependencies: EditorDependencies? = null
4649

4750
private val handler = Handler(Looper.getMainLooper())
4851
var filePathCallback: ValueCallback<Array<Uri?>?>? = null
@@ -130,7 +133,7 @@ class GutenbergView : WebView {
130133
this.settings.javaScriptEnabled = true
131134
this.settings.domStorageEnabled = true
132135
this.addJavascriptInterface(this, "editorDelegate")
133-
this.visibility = View.GONE
136+
this.visibility = GONE
134137

135138
this.webViewClient = object : WebViewClient() {
136139
override fun onReceivedError(
@@ -142,7 +145,7 @@ class GutenbergView : WebView {
142145
super.onReceivedError(view, request, error)
143146
}
144147

145-
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
148+
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
146149
super.onPageStarted(view, url, favicon)
147150
setGlobalJavaScriptVariables()
148151
}
@@ -259,8 +262,9 @@ class GutenbergView : WebView {
259262
}
260263
}
261264

262-
fun start(configuration: EditorConfiguration) {
265+
fun start(configuration: EditorConfiguration, dependencies: EditorDependencies? = null) {
263266
this.configuration = configuration
267+
this.dependencies = dependencies
264268

265269
// Set up asset caching if enabled
266270
if (configuration.enableAssetCaching) {
@@ -280,9 +284,7 @@ class GutenbergView : WebView {
280284

281285
initializeWebView()
282286

283-
val editorUrl = if (BuildConfig.GUTENBERG_EDITOR_URL.isNotEmpty()) {
284-
BuildConfig.GUTENBERG_EDITOR_URL
285-
} else {
287+
val editorUrl = BuildConfig.GUTENBERG_EDITOR_URL.ifEmpty {
286288
ASSET_URL
287289
}
288290

@@ -306,38 +308,16 @@ class GutenbergView : WebView {
306308
}
307309

308310
private fun setGlobalJavaScriptVariables() {
309-
val escapedTitle = encodeForEditor(configuration.title)
310-
val escapedContent = encodeForEditor(configuration.content)
311-
val editorSettings = configuration.editorSettings ?: "undefined"
312-
311+
val gbKit = GBKitGlobal.fromConfiguration(configuration, dependencies)
312+
val gbKitJson = gbKit.toJsonString()
313313
val gbKitConfig = """
314-
window.GBKit = {
315-
"siteApiRoot": "${configuration.siteApiRoot}",
316-
"siteApiNamespace": ${configuration.siteApiNamespace.joinToString(",", "[", "]") { "\"$it\"" }},
317-
"namespaceExcludedPaths": ${configuration.namespaceExcludedPaths.joinToString(",", "[", "]") { "\"$it\"" }},
318-
"authHeader": "${configuration.authHeader}",
319-
"themeStyles": ${configuration.themeStyles},
320-
"plugins": ${configuration.plugins},
321-
"hideTitle": ${configuration.hideTitle},
322-
"editorSettings": $editorSettings,
323-
"locale": "${configuration.locale}",
324-
${if (configuration.editorAssetsEndpoint != null) "\"editorAssetsEndpoint\": \"${configuration.editorAssetsEndpoint}\"," else ""}
325-
"enableNetworkLogging": ${configuration.enableNetworkLogging},
326-
"post": {
327-
"id": ${configuration.postId ?: -1},
328-
"title": "$escapedTitle",
329-
"content": "$escapedContent"
330-
}
331-
};
314+
window.GBKit = $gbKitJson;
332315
localStorage.setItem('GBKit', JSON.stringify(window.GBKit));
333316
""".trimIndent()
334317

335318
this.evaluateJavascript(gbKitConfig, null)
336319
}
337320

338-
private fun encodeForEditor(value: String): String {
339-
return java.net.URLEncoder.encode(value, "UTF-8").replace("+", "%20")
340-
}
341321

342322
fun clearConfig() {
343323
val jsCode = """
@@ -353,7 +333,7 @@ class GutenbergView : WebView {
353333
Log.e("GutenbergView", "You can't change the editor content until it has loaded")
354334
return
355335
}
356-
val encodedContent = encodeForEditor(newContent)
336+
val encodedContent = newContent.encodeForEditor()
357337
this.evaluateJavascript("editor.setContent('$encodedContent');", null)
358338
}
359339

@@ -362,7 +342,7 @@ class GutenbergView : WebView {
362342
Log.e("GutenbergView", "You can't change the editor content until it has loaded")
363343
return
364344
}
365-
val encodedTitle = encodeForEditor(newTitle)
345+
val encodedTitle = newTitle.encodeForEditor()
366346
this.evaluateJavascript("editor.setTitle('$encodedTitle');", null)
367347
}
368348

@@ -476,7 +456,7 @@ class GutenbergView : WebView {
476456
Log.e("GutenbergView", "You can't append text until the editor has loaded")
477457
return
478458
}
479-
val encodedText = encodeForEditor(text)
459+
val encodedText = text.encodeForEditor()
480460
handler.post {
481461
this.evaluateJavascript("editor.appendTextAtCursor(decodeURIComponent('$encodedText'));", null)
482462
}
@@ -490,7 +470,7 @@ class GutenbergView : WebView {
490470
if(!didFireEditorLoaded) {
491471
editorDidBecomeAvailableListener?.onEditorAvailable(this)
492472
this.didFireEditorLoaded = true
493-
this.visibility = View.VISIBLE
473+
this.visibility = VISIBLE
494474
this.alpha = 0f
495475
this.animate()
496476
.alpha(1f)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.wordpress.gutenberg
2+
3+
import java.net.URLEncoder
4+
5+
/**
6+
* Encodes a string for safe injection into the editor's JavaScript.
7+
*
8+
* This performs URL encoding and replaces `+` with `%20` to ensure spaces
9+
* are properly encoded for JavaScript string literals.
10+
*
11+
* @return The encoded string safe for editor injection.
12+
*/
13+
fun String.encodeForEditor(): String {
14+
return URLEncoder.encode(this, "UTF-8").replace("+", "%20")
15+
}

android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/EditorAssetBundle.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package org.wordpress.gutenberg.model
22

3+
import android.util.Log
34
import kotlinx.serialization.Serializable
45
import kotlinx.serialization.encodeToString
56
import kotlinx.serialization.json.Json
67
import java.io.File
78
import java.util.Date
89

10+
private const val TAG = "EditorAssetBundle"
11+
912
/**
1013
* A collection of editor assets downloaded from a WordPress site.
1114
*
@@ -154,6 +157,7 @@ data class EditorAssetBundle(
154157
val file = File(bundleRoot, EDITOR_REPRESENTATION_FILENAME)
155158
val data = json.encodeToString(representation)
156159
file.writeText(data)
160+
Log.d(TAG, "Wrote editor representation: file=${file.absolutePath}, size=${data.length} bytes")
157161
}
158162

159163
/**
@@ -193,6 +197,7 @@ data class EditorAssetBundle(
193197
val file = File(bundleRoot, MANIFEST_FILENAME)
194198
val data = json.encodeToString(rawBundle)
195199
file.writeText(data)
200+
Log.d(TAG, "Wrote asset bundle manifest: file=${file.absolutePath}, size=${data.length} bytes")
196201
}
197202

198203
override fun equals(other: Any?): Boolean {

android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/EditorPreloadList.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.wordpress.gutenberg.model
22

3+
import android.annotation.SuppressLint
4+
import kotlinx.serialization.Serializable
35
import kotlinx.serialization.encodeToString
46
import kotlinx.serialization.json.Json
57
import kotlinx.serialization.json.JsonElement
@@ -18,6 +20,8 @@ import org.wordpress.gutenberg.model.http.asPreloadResponse
1820
* The preload list is serialized to JSON and passed to the editor's JavaScript, which uses
1921
* these cached responses instead of making network calls.
2022
*/
23+
@SuppressLint("UnsafeOptInUsageError")
24+
@Serializable
2125
@ConsistentCopyVisibility
2226
data class EditorPreloadList private constructor(
2327
/** The ID of the post being edited, if editing an existing post. */

android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/GBKitGlobal.kt

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.wordpress.gutenberg.model
22

3+
import android.annotation.SuppressLint
34
import kotlinx.serialization.Serializable
45
import kotlinx.serialization.encodeToString
56
import kotlinx.serialization.json.Json
67
import kotlinx.serialization.json.JsonElement
7-
import org.wordpress.gutenberg.model.EditorConfiguration
8-
import java.net.URLEncoder
8+
import org.wordpress.gutenberg.encodeForEditor
99

1010
/**
1111
* Configuration object passed to the editor's JavaScript as a global variable.
@@ -14,6 +14,7 @@ import java.net.URLEncoder
1414
* providing the JavaScript code with all the information it needs to initialize
1515
* the editor and communicate with the WordPress REST API.
1616
*/
17+
@SuppressLint("UnsafeOptInUsageError")
1718
@Serializable
1819
data class GBKitGlobal(
1920
/** The site's base URL, or `null` if offline mode is enabled. */
@@ -56,10 +57,10 @@ data class GBKitGlobal(
5657
val logLevel: String = "warn",
5758
/** Whether to log network requests in the JavaScript console. */
5859
val enableNetworkLogging: Boolean,
59-
/** The raw editor settings from the WordPress REST API. */
60+
/** The raw editor settings JSON from the WordPress REST API. */
6061
val editorSettings: JsonElement?,
61-
/** Pre-fetched API responses for faster editor initialization. */
62-
val preloadData: JsonElement?
62+
/** Pre-fetched API responses JSON for faster editor initialization. */
63+
val preloadData: JsonElement? = null
6364
) {
6465
/**
6566
* The post data passed to the editor.
@@ -85,7 +86,7 @@ data class GBKitGlobal(
8586
*/
8687
fun fromConfiguration(
8788
configuration: EditorConfiguration,
88-
dependencies: EditorDependencies
89+
dependencies: EditorDependencies?
8990
): GBKitGlobal {
9091
return GBKitGlobal(
9192
siteURL = configuration.siteURL.ifEmpty { null },
@@ -99,18 +100,14 @@ data class GBKitGlobal(
99100
locale = configuration.locale ?: "en",
100101
post = Post(
101102
id = configuration.postId ?: -1,
102-
title = urlEncode(configuration.title),
103-
content = urlEncode(configuration.content)
103+
title = configuration.title.encodeForEditor(),
104+
content = configuration.content.encodeForEditor()
104105
),
105106
enableNetworkLogging = configuration.enableNetworkLogging,
106-
editorSettings = dependencies.editorSettings.jsonValue,
107-
preloadData = dependencies.preloadList?.build()
107+
editorSettings = dependencies?.editorSettings?.jsonValue,
108+
preloadData = dependencies?.preloadList?.build()
108109
)
109110
}
110-
111-
private fun urlEncode(value: String): String {
112-
return URLEncoder.encode(value, "UTF-8")
113-
}
114111
}
115112

116113
/**

0 commit comments

Comments
 (0)