From 36380fd665c207ccf372dd7deb56c973e64ed095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=BCegg?= Date: Fri, 25 Sep 2026 22:03:41 +0200 Subject: [PATCH] fix(appstore): do not wrap app store screenshots in the proxy twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app store now serves screenshot URLs already pointing at usercontent.apps.nextcloud.com. Wrapping them again names a file the proxy never stores the image under, so no app shows its screenshot. Pass such URLs through unchanged, and encode the others with the URL-safe alphabet the proxy stores its files under. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Martin Rüegg (cherry picked from commit dc341b43e24acc4e783b8c42aa4dc56d8045bd13) --- .../lib/Controller/AppSettingsController.php | 14 +++++++---- .../Controller/AppSettingsControllerTest.php | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index 1f77ca5b0a5b2..a450748114fe9 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -53,6 +53,8 @@ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class AppSettingsController extends Controller { + private const USERCONTENT_PROXY_URL = 'https://usercontent.apps.nextcloud.com/'; + /** @var array */ private $allApps = []; @@ -299,12 +301,16 @@ private function getAllCategories() { /** * Convert URL to proxied URL so CSP is no problem + * + * The app store already serves its screenshot URLs in this form; only an + * app's own info.xml still names the source. */ private function createProxyPreviewUrl(string $url): string { - if ($url === '') { - return ''; + if ($url === '' || str_starts_with($url, self::USERCONTENT_PROXY_URL)) { + return $url; } - return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url); + // The proxy stores its files under the URL-safe alphabet + return self::USERCONTENT_PROXY_URL . strtr(base64_encode($url), '+/', '-_'); } private function fetchApps() { @@ -531,7 +537,7 @@ private function getAppsForCategory($requestedCategory = ''): array { 'missingMaxOwnCloudVersion' => false, 'missingMinOwnCloudVersion' => false, 'canInstall' => true, - 'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '', + 'screenshot' => $this->createProxyPreviewUrl($app['screenshots'][0]['url'] ?? ''), 'score' => $app['ratingOverall'], 'ratingNumOverall' => $app['ratingNumOverall'], 'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5, diff --git a/apps/settings/tests/Controller/AppSettingsControllerTest.php b/apps/settings/tests/Controller/AppSettingsControllerTest.php index b6d5d8c5dd0b5..2e7e4b1821b48 100644 --- a/apps/settings/tests/Controller/AppSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AppSettingsControllerTest.php @@ -238,4 +238,27 @@ public function testViewAppsAppstoreNotEnabled(): void { $this->assertEquals($expected, $this->appSettingsController->viewApps()); } + + public static function dataCreateProxyPreviewUrl(): array { + return [ + 'empty' => ['', ''], + 'already proxied by the app store' => [ + 'https://usercontent.apps.nextcloud.com/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw==', + 'https://usercontent.apps.nextcloud.com/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw==', + ], + 'source URL' => [ + 'https://example.org/a.png', + 'https://usercontent.apps.nextcloud.com/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw==', + ], + 'source URL encoding to + and /' => [ + 'https://example.org/?a=>>>', + 'https://usercontent.apps.nextcloud.com/aHR0cHM6Ly9leGFtcGxlLm9yZy8_YT0-Pj4=', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataCreateProxyPreviewUrl')] + public function testCreateProxyPreviewUrl(string $url, string $expected): void { + $this->assertSame($expected, self::invokePrivate($this->appSettingsController, 'createProxyPreviewUrl', [$url])); + } }