diff --git a/apps/appstore/lib/Controller/ApiController.php b/apps/appstore/lib/Controller/ApiController.php index ae04d000d5f41..039ffa30f10c2 100644 --- a/apps/appstore/lib/Controller/ApiController.php +++ b/apps/appstore/lib/Controller/ApiController.php @@ -43,6 +43,8 @@ class ApiController extends OCSController { private const int OFFICIAL_APP_LEVEL = 200; + private const string USERCONTENT_PROXY_URL = 'https://usercontent.apps.nextcloud.com/'; + /** @var array */ private $allApps = []; @@ -305,13 +307,17 @@ public function enableBundle(string $bundleId): DataResponse { /** * 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(): void { @@ -544,7 +550,7 @@ private function getAppsForCategory(string $requestedCategory = ''): array { $phpDependencies ), 'level' => ($app['isFeatured'] === true) ? 200 : 100, - 'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '', + 'screenshot' => $this->createProxyPreviewUrl($app['screenshots'][0]['url'] ?? ''), 'ratingOverall' => $app['ratingOverall'], 'ratingNumOverall' => $app['ratingNumOverall'], 'removable' => $existsLocally, diff --git a/apps/appstore/tests/Controller/ApiControllerTest.php b/apps/appstore/tests/Controller/ApiControllerTest.php index 98bc537d9b976..50d28df9c2e89 100644 --- a/apps/appstore/tests/Controller/ApiControllerTest.php +++ b/apps/appstore/tests/Controller/ApiControllerTest.php @@ -105,4 +105,27 @@ public function testListCategories(): void { $jsonResponse = json_encode($response->getData()); $this->assertJsonStringEqualsJsonFile(__DIR__ . '/../fixtures/categories-api-response.json', $jsonResponse); } + + 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('dataCreateProxyPreviewUrl')] + public function testCreateProxyPreviewUrl(string $url, string $expected): void { + $this->assertSame($expected, self::invokePrivate($this->apiController, 'createProxyPreviewUrl', [$url])); + } }