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
1 change: 1 addition & 0 deletions android/Gutenberg/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ID>ExplicitItLambdaParameter:EditorAssetsLibrary.kt$EditorAssetsLibrary${ str, it -&gt; str + "%02x".format(it) }</ID>
<ID>FunctionNaming:EditorURLCache.kt$EditorURLCache$private fun __store( response: EditorURLResponse, url: String, httpMethod: EditorHttpMethod, currentDate: Date )</ID>
<ID>LargeClass:GutenbergView.kt$GutenbergView : FrameLayout</ID>
<ID>LargeClass:MediaUploadServerTest.kt$MediaUploadServerTest</ID>
<ID>LongMethod:FixtureTests.kt$FixtureTests$@Test fun `request parsing - all basic cases pass`()</ID>
<ID>LongMethod:FixtureTests.kt$FixtureTests$@Test fun `request parsing - all incremental cases pass`()</ID>
<ID>LongMethod:HTTPRequestParser.kt$HTTPRequestParser$fun append(data: ByteArray): Unit</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,44 @@ class GutenbergView : FrameLayout {
var requestInterceptor: GutenbergRequestInterceptor = DefaultGutenbergRequestInterceptor()

/**
* Optional delegate for customizing media upload behavior (resize, transcode,
* custom upload).
* Transforms media (resize, transcode, …) before GutenbergKit delivers it to
* the configured site. The safe, common extension point — a processor never
* performs the upload itself, so it cannot deliver media to the wrong place.
*
* Provide this **before the editor loads** — typically right after
* construction (e.g. in the `AndroidView` factory). It is captured once, when
* the page begins loading, and advertised to the page then; setting it
* afterward has no effect, so the setter throws to surface the mistake.
* the page begins loading; setting it afterward has no effect, so the setter
* throws to surface the mistake.
*/
var mediaUploadDelegate: MediaUploadDelegate? = null
var mediaProcessor: MediaProcessor? = null
set(value) {
check(!hasStartedLoading) {
"mediaUploadDelegate must be set before the editor loads (e.g. right " +
"after construction). It is captured when the page begins loading; " +
"setting it afterward has no effect."
}
check(!hasStartedLoading) { lateMediaAssignmentMessage("mediaProcessor") }
field = value
}

/**
* Takes over media upload on the host's own stack (background service, offline
* queue, resumable transport). Setting it makes the host own every upload and
* its whole lifecycle; GutenbergKit stays out of the network entirely for media.
*
* Same lifecycle rules as [mediaProcessor]: set it before the editor loads.
*/
var mediaUploader: MediaUploader? = null
set(value) {
check(!hasStartedLoading) { lateMediaAssignmentMessage("mediaUploader") }
field = value
}

private fun lateMediaAssignmentMessage(name: String) =
"$name must be set before the editor loads (e.g. right after construction). " +
"It is captured when the page begins loading; setting it afterward has no effect."

@Volatile private var uploadServer: MediaUploadServer? = null

/**
* True once the editor page has begun loading and the upload server's
* configuration has been captured. After this the [mediaUploadDelegate] can no
* longer take effect, so its setter throws.
* configuration has been captured. After this the [mediaProcessor]/[mediaUploader]
* can no longer take effect, so their setters throw.
*/
@Volatile private var hasStartedLoading = false

Expand Down Expand Up @@ -638,13 +652,13 @@ class GutenbergView : FrameLayout {

/**
* Invoked when the editor page begins loading. Starts the upload server once —
* capturing the [mediaUploadDelegate] provided before load — then advertises
* the editor globals (including the server's port and token) to the page.
* capturing the [mediaProcessor]/[mediaUploader] provided before load — then
* advertises the editor globals (including the server's port and token) to the page.
*
* Starting the server here, on the UI thread, rather than from the
* [mediaUploadDelegate] setter keeps its whole lifecycle — start here, stop in
* [onDetachedFromWindow] — on the UI thread, so it can't race a
* background-thread delegate assignment.
* [mediaProcessor]/[mediaUploader] setters keeps its whole lifecycle — start
* here, stop in [onDetachedFromWindow] — on the UI thread, so it can't race a
* background-thread assignment.
*/
private fun onEditorPageStarted() {
if (!hasStartedLoading) {
Expand All @@ -671,17 +685,22 @@ class GutenbergView : FrameLayout {
}

private fun startUploadServer() {
// No delegate means nothing wants to customize uploads, so there's no reason
// to route them through the native server — leave it down and let uploads
// fall to the default WebView path. (Matches iOS.)
if (mediaUploadDelegate == null) return

// The native upload server relays through DefaultMediaUploader, which needs a
// site root and an auth header (every host provides one — the editor injects
// it because the WebView has no auth cookies). Without both there is nothing
// to upload through, so leave the server down and let uploads fall to the
// default WebView path rather than start a server that could only fail.
if (configuration.siteApiRoot.isEmpty() || configuration.authHeader.isEmpty()) return
// Nothing to route through the native server unless the host provided a
// processor or an uploader. (Matches iOS.)
if (mediaProcessor == null && mediaUploader == null) return

// A DefaultMediaUploader delivers GutenbergKit-owned uploads (when no uploader
// is set) and relays the editor's media DELETEs to the configured site — every
// attachment lives there, even one a host uploader delivered. It needs a site
// root and an auth header (every host provides one — the editor injects it
// because the WebView has no auth cookies). If GutenbergKit would have to
// deliver uploads itself but lacks those, there's nothing to upload through, so
// leave the server down and let uploads fall to the default WebView path.
if (mediaUploader == null &&
(configuration.siteApiRoot.isEmpty() || configuration.authHeader.isEmpty())
) {
return
}

// The editor reaches the loopback server over cleartext http://localhost. If
// the host app's network-security config doesn't permit cleartext to
Expand All @@ -701,14 +720,24 @@ class GutenbergView : FrameLayout {
}

try {
val defaultUploader = DefaultMediaUploader(
httpClient = uploadHttpClient,
siteApiRoot = configuration.siteApiRoot,
authHeader = configuration.authHeader,
siteApiNamespace = configuration.siteApiNamespace.toList()
)
// Build a DefaultMediaUploader whenever there are credentials to reach the
// site: it delivers GutenbergKit-owned uploads and relays the editor's
// DELETEs there. null only when a host owns uploads and no creds exist.
val defaultUploader = if (
configuration.siteApiRoot.isNotEmpty() && configuration.authHeader.isNotEmpty()
) {
DefaultMediaUploader(
httpClient = uploadHttpClient,
siteApiRoot = configuration.siteApiRoot,
authHeader = configuration.authHeader,
siteApiNamespace = configuration.siteApiNamespace.toList()
)
} else {
null
}
uploadServer = MediaUploadServer(
uploadDelegate = mediaUploadDelegate,
processor = mediaProcessor,
uploader = mediaUploader,
defaultUploader = defaultUploader,
cacheDir = context.cacheDir,
scope = coroutineScope
Expand Down
Loading
Loading