From 6ff4df3088e568668f22224e924a1737ddbba0a0 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Mon, 20 Oct 2025 17:48:18 -0700 Subject: [PATCH 1/2] content test: Add explicit test case for Zulip Cloud external image preview This kind of URL is already covered in the tests of *clusters* of image previews, but it seems helpful to isolate it like this with its own comment. We're also about to add a third variant, testing that we accept images generated with an arbitrary CAMO_URI, which actually we don't currently do, so that'll be a regression test. --- test/model/content_test.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 59a66f7ce8..430fa69703 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -785,8 +785,8 @@ class ContentExample { ]), ]); - static const imagePreviewSingleExternal = ContentExample( - 'single image preview external', + static const imagePreviewSingleExternal1 = ContentExample( + 'single image preview external, src starts with /external_content', // https://chat.zulip.org/#narrow/stream/7-test-here/topic/Greg/near/1892172 "https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg", '
' @@ -799,6 +799,21 @@ class ContentExample { ]), ]); + static const imagePreviewSingleExternal2 = ContentExample( + 'single image preview external, src starts with https://uploads.zulipusercontent.net/', + // Zulip Cloud has CAMO_URI = "https://uploads.zulipusercontent.net/"; + // this example is from a DM on a closed Zulip Cloud org. + "https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg", + '
' + '' + '
', [ + ImagePreviewNodeList([ + ImagePreviewNode(srcUrl: 'https://uploads.zulipusercontent.net/99742b0f992be15283c428dd42f3b9f5db138d69/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067', + thumbnailUrl: null, loading: false, + originalWidth: null, originalHeight: null), + ]), + ]); + static const imagePreviewInvalidUrl = ContentExample( 'single image preview with invalid URL', null, // hypothetical, to test for a risk of crashing @@ -1816,7 +1831,8 @@ void main() async { testParseExample(ContentExample.imagePreviewSingleNoDimensions); testParseExample(ContentExample.imagePreviewSingleNoThumbnail); testParseExample(ContentExample.imagePreviewSingleLoadingPlaceholder); - testParseExample(ContentExample.imagePreviewSingleExternal); + testParseExample(ContentExample.imagePreviewSingleExternal1); + testParseExample(ContentExample.imagePreviewSingleExternal2); testParseExample(ContentExample.imagePreviewInvalidUrl); testParseExample(ContentExample.imagePreviewCluster); testParseExample(ContentExample.imagePreviewClusterNoThumbnails); From 0c7edc08bdaec3261dabb182dd0a248b0e876572 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Mon, 20 Oct 2025 16:09:44 -0700 Subject: [PATCH 2/2] content: Relax image-preview parsing, notably to accept arbitrary CAMO_URI See discussion: https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279454 We already test the `src == href` case, as ContentExample.imagePreviewSingleNoThumbnail, but I added a test case for an arbitrary CAMO_URI that fails before this commit. --- lib/model/content.dart | 19 +++++++------------ test/model/content_test.dart | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 843b9ddda9..52fa173bc2 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -1413,22 +1413,17 @@ class _ZulipContentParser { final String srcUrl; final String? thumbnailUrl; if (src.startsWith('/user_uploads/thumbnail/')) { + // For why we recognize this as the thumbnail form, see discussion: + // https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279872 srcUrl = href; thumbnailUrl = src; - } else if (src.startsWith('/external_content/') - || src.startsWith('https://uploads.zulipusercontent.net/')) { - // This image preview uses camo, which still happens on current servers - // (2025-10); discussion: - // https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279235 - srcUrl = src; - thumbnailUrl = null; - } else if (href == src) { - // Probably generated by a server before the thumbnailing feature landed: - // https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279234 + } else { + // Known cases this handles: + // - `src` starts with CAMO_URI, a server variable (e.g. on Zulip Cloud + // it's "https://uploads.zulipusercontent.net/" in 2025-10). + // - `src` matches `href`, e.g. from pre-thumbnailing servers. srcUrl = src; thumbnailUrl = null; - } else { - return UnimplementedBlockContentNode(htmlNode: divElement); } double? originalWidth, originalHeight; diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 430fa69703..bab9b05969 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -814,6 +814,21 @@ class ContentExample { ]), ]); + static const imagePreviewSingleExternal3 = ContentExample( + 'single image preview external, src starts with https://custom.camo-uri.example/', + // CAMO_URI (server variable) can be set arbitrarily; + // for another possible value, see imagePreviewSingleExternal2. + "https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg", + '
' + '' + '
', [ + ImagePreviewNodeList([ + ImagePreviewNode(srcUrl: 'https://custom.camo-uri.example/99742b0f992be15283c428dd42f3b9f5db138d69/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067', + thumbnailUrl: null, loading: false, + originalWidth: null, originalHeight: null), + ]), + ]); + static const imagePreviewInvalidUrl = ContentExample( 'single image preview with invalid URL', null, // hypothetical, to test for a risk of crashing @@ -1833,6 +1848,7 @@ void main() async { testParseExample(ContentExample.imagePreviewSingleLoadingPlaceholder); testParseExample(ContentExample.imagePreviewSingleExternal1); testParseExample(ContentExample.imagePreviewSingleExternal2); + testParseExample(ContentExample.imagePreviewSingleExternal3); testParseExample(ContentExample.imagePreviewInvalidUrl); testParseExample(ContentExample.imagePreviewCluster); testParseExample(ContentExample.imagePreviewClusterNoThumbnails);