Skip to content
Open
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 @@ -113,7 +113,7 @@ class GutenbergView : FrameLayout {
var requestInterceptor: GutenbergRequestInterceptor = DefaultGutenbergRequestInterceptor()

/**
* Optional delegate for transforming media before upload (resize, transcode,
* Optional processor that transforms media before upload (resize, transcode,
* strip EXIF).
*
* To perform the upload yourself, set [mediaUploader] instead.
Expand All @@ -123,9 +123,9 @@ class GutenbergView : FrameLayout {
* the page begins loading, and advertised to the page then; 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) { lateMediaAssignmentMessage("mediaUploadDelegate") }
check(!hasStartedLoading) { lateMediaAssignmentMessage("mediaProcessor") }
field = value
}

Expand All @@ -134,11 +134,11 @@ class GutenbergView : FrameLayout {
* 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 [mediaUploadDelegate]: set it before the editor loads,
* Same lifecycle rules as [mediaProcessor]: set it before the editor loads,
* and this view owns it for its lifetime — so you needn't retain it yourself, just
* don't strongly retain this [GutenbergView] from your uploader.
*
* A [mediaUploadDelegate] can still transform the file first; only delivery moves
* A [mediaProcessor] can still transform the file first; only delivery moves
* to the uploader.
*/
var mediaUploader: MediaUploader? = null
Expand All @@ -155,7 +155,7 @@ class GutenbergView : FrameLayout {

/**
* True once the editor page has begun loading and the upload server's
* configuration has been captured. After this the [mediaUploadDelegate] can no
* configuration has been captured. After this the [mediaProcessor] can no
* longer take effect, so its setter throws.
*/
@Volatile private var hasStartedLoading = false
Expand Down Expand Up @@ -663,13 +663,13 @@ class GutenbergView : FrameLayout {

/**
* Invoked when the editor page begins loading. Starts the upload server once —
* capturing the [mediaUploadDelegate] provided before load — then advertises
* capturing the [mediaProcessor] 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
* [mediaProcessor] setter keeps its whole lifecycle — start here, stop in
* [onDetachedFromWindow] — on the UI thread, so it can't race a
* background-thread delegate assignment.
* background-thread processor assignment.
*/
private fun onEditorPageStarted() {
if (!hasStartedLoading) {
Expand Down Expand Up @@ -697,9 +697,9 @@ class GutenbergView : FrameLayout {

private fun startUploadServer() {
// Nothing to route through the native server unless the host provided a
// delegate or an uploader — leave it down and let uploads fall to the default
// processor or an uploader — leave it down and let uploads fall to the default
// WebView path. (Matches iOS.)
if (mediaUploadDelegate == null && mediaUploader == null) return
if (mediaProcessor == null && mediaUploader == null) return

// The native upload server relays through InternalMediaClient, which needs a
// site root and an auth header (every host provides one — the editor injects
Expand Down Expand Up @@ -733,7 +733,7 @@ class GutenbergView : FrameLayout {
siteApiNamespace = configuration.siteApiNamespace.toList()
)
uploadServer = MediaUploadServer(
uploadDelegate = mediaUploadDelegate,
processor = mediaProcessor,
internalClient = internalClient,
uploader = mediaUploader,
cacheDir = context.cacheDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ internal class MediaUploadResponse(
)

/**
* The result of a delegate's [MediaUploadDelegate.processFile].
* The result of a processor's [MediaProcessor.processFile].
*/
sealed class ProcessedProxyFile {
/** The delegate did not modify the file; the original upload is forwarded unchanged. */
/** The processor did not modify the file; the original upload is forwarded unchanged. */
data object Original : ProcessedProxyFile()

/**
* The delegate produced a file to upload, along with its MIME type and
* The processor produced a file to upload, along with its MIME type and
* filename. Both are used verbatim, so a format change (e.g. transcoding MOV
* to MP4, or an in-place EXIF strip) must report the resulting type and
* filename for WordPress to store the file correctly.
Expand All @@ -75,23 +75,23 @@ sealed class ProcessedProxyFile {
/**
* Transforms media before GutenbergKit delivers it.
*
* A delegate only changes *bytes* — GutenbergKit still uploads the result to the
* A processor only changes *bytes* — GutenbergKit still uploads the result to the
* configured site and owns the whole lifecycle (retries, cleanup). Because it never
* performs the upload itself, it cannot deliver media to the wrong place. Set
* [GutenbergView.mediaUploadDelegate] to resize images, transcode video, strip EXIF,
* [GutenbergView.mediaProcessor] to resize images, transcode video, strip EXIF,
* etc.
*
* This is the safe, common extension point: most hosts want only this. To perform the
* upload yourself, implement [MediaUploader] instead.
*/
interface MediaUploadDelegate {
interface MediaProcessor {
/**
* Whether this delegate might transform a file with the given metadata.
* Whether this processor might transform a file with the given metadata.
*
* A cheap, metadata-only gate the server consults *before* materializing the
* upload to a temp file. Return false to decline a file by type — e.g. an
* image-only delegate returning false for a video — so the server forwards
* the original upload to WordPress without first copying a file the delegate
* image-only processor returning false for a video — so the server forwards
* the original upload to WordPress without first copying a file the processor
* won't touch.
*
* With a [MediaUploader] set this can't decline the upload itself — an uploader
Expand Down Expand Up @@ -130,7 +130,7 @@ data class MediaUploadField(val name: String, val value: String)
* Everything a [MediaUploader] needs to reproduce a native upload: the file to send,
* its metadata, the editor's non-file form fields, and the request's query.
*
* @property file The file to upload — already processed, if a [MediaUploadDelegate] ran.
* @property file The file to upload — already processed, if a [MediaProcessor] ran.
* @property mimeType The file's MIME type.
* @property filename The file's name.
* @property fields The editor's non-file form fields, in order, each decoded as UTF-8 —
Expand Down Expand Up @@ -203,7 +203,7 @@ interface MediaUploader {
* stop on detach.
*/
internal class MediaUploadServer(
private val uploadDelegate: MediaUploadDelegate?,
private val processor: MediaProcessor?,
private val internalClient: InternalMediaClient?,
private val uploader: MediaUploader? = null,
cacheDir: File? = null,
Expand Down Expand Up @@ -364,25 +364,25 @@ internal class MediaUploadServer(
val mimeType = filePart.contentType
val filename = filePart.filename ?: "upload"

// Ask the delegate — from metadata alone — whether it will touch a file
// Ask the processor — from metadata alone — whether it will touch a file
// like this. If not, forward the original upload to WordPress directly,
// skipping a full temp-file copy of a file the delegate won't process
// (e.g. a video handed to an image-only delegate).
// skipping a full temp-file copy of a file the processor won't process
// (e.g. a video handed to an image-only processor).
// An uploader takes over delivery for *every* file, so with one set there is no
// passthrough to fall to and the gate can't decline the upload outright. It
// still decides whether processFile runs, though — a declined file is handed to
// the uploader unprocessed rather than to a delegate that said it won't touch it
// the uploader unprocessed rather than to a processor that said it won't touch it
// — so the answer is carried into processAndUpload rather than short-circuited
// away here. Asked exactly once per upload, matching iOS.
val delegateWantsFile = uploadDelegate?.handlesFile(mimeType, filename) == true
if (uploader == null && !delegateWantsFile) {
val processorWantsFile = processor?.handlesFile(mimeType, filename) == true
if (uploader == null && !processorWantsFile) {
return passthroughResponse(request, query)
}

val tempFile = writePartToTempFile(filePart)
?: return errorResponse(500, "Failed to save file")

return processAndRespond(request, tempFile, filePart, extraParts, query, delegateWantsFile)
return processAndRespond(request, tempFile, filePart, extraParts, query, processorWantsFile)
}

@Suppress("TooGenericExceptionCaught")
Expand Down Expand Up @@ -410,7 +410,7 @@ internal class MediaUploadServer(
* The response's own `Content-Type` wins over the JSON default, matched
* case-insensitively — HTTP header names are case-insensitive, and
* [HttpResponse] serializes every entry it is given, so a plain map merge
* would emit the name twice for a delegate that spells it `content-type`.
* would emit the name twice for a processor that spells it `content-type`.
*/
private fun relayResponse(response: MediaUploadResponse): HttpResponse {
val hasContentType = response.headers.keys.any { it.lowercase() == "content-type" }
Expand Down Expand Up @@ -466,20 +466,20 @@ internal class MediaUploadServer(
@Suppress("TooGenericExceptionCaught")
private suspend fun processAndRespond(
request: HttpRequest, tempFile: File, filePart: MultipartPart,
extraParts: List<MultipartPart>, query: String, delegateWantsFile: Boolean
extraParts: List<MultipartPart>, query: String, processorWantsFile: Boolean
): HttpResponse {
try {
val uploadResult = processAndUpload(
tempFile, filePart.contentType, filePart.filename ?: "upload",
extraParts, query, delegateWantsFile
extraParts, query, processorWantsFile
)
val response = when (uploadResult) {
is UploadResult.Uploaded -> {
Log.d(TAG, "Uploaded file to WordPress")
uploadResult.response
}
is UploadResult.Passthrough -> {
// Delegate didn't modify the file — forward the original
// The processor didn't modify the file — forward the original
// request body to WordPress without re-encoding.
Log.d(TAG, "Passthrough: forwarding original request body to WordPress")
performPassthroughUpload(request, query)
Expand All @@ -493,7 +493,7 @@ internal class MediaUploadServer(
throw e // Never swallow coroutine cancellation.
} catch (e: Exception) {
// Any other failure — IOException from the upload call, JSON parse
// errors, a throwing host delegate, or "no internal media client
// errors, a throwing host processor, or "no internal media client
// configured" — must still be answered WITH CORS headers. Otherwise
// it escapes to HttpServer's header-less 500 fallback and the browser
// rejects the preflighted cross-origin fetch with an opaque "Failed to
Expand All @@ -506,7 +506,7 @@ internal class MediaUploadServer(
}
}

// MARK: - Delegate Pipeline
// MARK: - Processor Pipeline

private sealed class UploadResult {
data class Uploaded(val response: MediaUploadResponse) : UploadResult()
Expand All @@ -527,21 +527,21 @@ internal class MediaUploadServer(

private suspend fun processAndUpload(
file: File, mimeType: String, filename: String,
extraParts: List<MultipartPart>, query: String, delegateWantsFile: Boolean
extraParts: List<MultipartPart>, query: String, processorWantsFile: Boolean
): UploadResult {
// Process (resize, transcode, etc.) — but only for a file the delegate's
// metadata gate accepted. handlesFile returning false is the delegate saying it
// Process (resize, transcode, etc.) — but only for a file the processor's
// metadata gate accepted. handlesFile returning false is the processor saying it
// won't touch a file like this, so handing it one anyway would break the
// contract the gate documents. With an uploader set the file still gets
// delivered; it just skips processing on its way there.
val processed = if (delegateWantsFile) {
uploadDelegate?.processFile(file, mimeType, filename) ?: ProcessedProxyFile.Original
val processed = if (processorWantsFile) {
processor?.processFile(file, mimeType, filename) ?: ProcessedProxyFile.Original
} else {
ProcessedProxyFile.Original
}

// Resolve the file to upload and its metadata. Processed uses the
// delegate's values verbatim, so a format change is reported to WordPress.
// processor's values verbatim, so a format change is reported to WordPress.
val targetFile: File
val targetMimeType: String
val targetFilename: String
Expand Down Expand Up @@ -590,7 +590,7 @@ internal class MediaUploadServer(
?: error("No media uploader or internal media client configured")
return UploadResult.Uploaded(result)
} finally {
// The processed file (if the delegate produced a new one) is ours to
// The processed file (if the processor produced a new one) is ours to
// clean up — covers the success and throw paths alike.
if (targetFile != file) {
targetFile.delete()
Expand Down Expand Up @@ -718,7 +718,7 @@ internal open class InternalMediaClient(
/**
* Forwards the original request body to WordPress without re-encoding.
*
* Used when the delegate's `processFile` returned the file unchanged —
* Used when the processor's `processFile` returned the file unchanged —
* the incoming multipart body is already valid for WordPress.
*/
open suspend fun passthroughUpload(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class GutenbergViewUploadServerTest {
val view = makeView()
try {
// A delegate provided before load is captured when the page starts.
view.mediaUploadDelegate = mock(MediaUploadDelegate::class.java)
view.mediaProcessor = mock(MediaProcessor::class.java)
startLoading(view)
idle()
assertNotNull(
Expand Down Expand Up @@ -136,7 +136,7 @@ class GutenbergViewUploadServerTest {
// The delegate is captured at load; a later assignment is a programmer
// error and must surface loudly rather than silently do nothing.
assertThrows(IllegalStateException::class.java) {
view.mediaUploadDelegate = mock(MediaUploadDelegate::class.java)
view.mediaProcessor = mock(MediaProcessor::class.java)
}
} finally {
detach(view)
Expand All @@ -146,7 +146,7 @@ class GutenbergViewUploadServerTest {
@Test
fun `detaching the view stops and clears the upload server`() {
val view = makeView()
view.mediaUploadDelegate = mock(MediaUploadDelegate::class.java)
view.mediaProcessor = mock(MediaProcessor::class.java)
startLoading(view)
idle()
assertNotNull(uploadServerOf(view))
Expand Down
Loading
Loading