From f5682c05169e0eef88f24d0650c4255e790097b9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 13:23:34 +0530 Subject: [PATCH 1/8] chore: add support for svg favicons --- app/controllers/api/avatars.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 3a7b4aa582..5083142a7e 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -362,7 +362,8 @@ App::get('/v1/avatars/favicon') $client = new Client(); try { $res = $client - ->setAllowRedirects(false) + ->setAllowRedirects(true) + ->setMaxRedirects(5) ->setUserAgent(\sprintf( APP_USERAGENT, System::getEnv('_APP_VERSION', 'UNKNOWN'), @@ -399,6 +400,11 @@ App::get('/v1/avatars/favicon') $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); switch ($ext) { + case 'svg': + $space = PHP_INT_MAX; + $outputHref = $absolute; + $outputExt = $ext; + break; case 'ico': case 'png': case 'jpg': @@ -437,7 +443,8 @@ App::get('/v1/avatars/favicon') $client = new Client(); try { $res = $client - ->setAllowRedirects(false) + ->setAllowRedirects(true) + ->setMaxRedirects(5) ->fetch($outputHref); } catch (\Throwable) { throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); @@ -450,7 +457,7 @@ App::get('/v1/avatars/favicon') $data = $res->getBody(); if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - if (empty($data) || (\mb_substr($data, 0, 5) === 'file($data); } + if ('svg' == $outputExt) { // Skip crop, Imagick isn\'t supporting svg files + $response + ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days + ->setContentType('image/svg+xml') + ->file($data); + return; + } + $image = new Image($data); $image->crop((int) $width, (int) $height); $output = (empty($output)) ? $type : $output; From 4adebc8ba222c1c3a7b8db4e5345aa9b66235edc Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 13:33:10 +0530 Subject: [PATCH 2/8] chore: add svg sanitization --- app/controllers/api/avatars.php | 12 ++++- composer.json | 3 +- composer.lock | 83 +++++++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 5083142a7e..b0eaddd742 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -401,6 +401,7 @@ App::get('/v1/avatars/favicon') switch ($ext) { case 'svg': + // SVG icons are prioritized by assigning the maximum possible value. $space = PHP_INT_MAX; $outputHref = $absolute; $outputExt = $ext; @@ -457,7 +458,11 @@ App::get('/v1/avatars/favicon') $data = $res->getBody(); if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - if (empty($data) || str_starts_with($data, 'sanitize($data); + if ($cleanSvg === false) { + throw new \Exception('SVG sanitization failed'); + } $response ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days ->setContentType('image/svg+xml') diff --git a/composer.json b/composer.json index 31a31af9f2..73cdcc3d86 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,8 @@ "adhocore/jwt": "1.1.*", "spomky-labs/otphp": "^10.0", "webonyx/graphql-php": "14.11.*", - "league/csv": "9.14.*" + "league/csv": "9.14.*", + "enshrined/svg-sanitize": "0.21.*" }, "require-dev": { "ext-fileinfo": "*", diff --git a/composer.lock b/composer.lock index da084c8fcd..aafe1d216c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "edbe5912c45e1f467f398541a75a77de", + "content-hash": "7b2ef6192403daf5c492219822ce0aa1", "packages": [ { "name": "adhocore/jwt", @@ -69,16 +69,16 @@ }, { "name": "appwrite/appwrite", - "version": "15.0.0", + "version": "15.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf" + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/deb97b62e0abed8a4fd5c5d48e77365cf89867cf", - "reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b", + "reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b", "shasum": "" }, "require": { @@ -104,10 +104,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/15.0.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-05-18T09:47:10+00:00" + "time": "2025-08-01T04:50:51+00:00" }, { "name": "appwrite/php-clamav", @@ -628,6 +628,51 @@ ], "time": "2023-08-10T19:36:49+00:00" }, + { + "name": "enshrined/svg-sanitize", + "version": "0.21.0", + "source": { + "type": "git", + "url": "https://github.com/darylldoyle/svg-sanitizer.git", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "enshrined\\svgSanitize\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Daryll Doyle", + "email": "daryll@enshrined.co.uk" + } + ], + "description": "An SVG sanitizer for PHP", + "support": { + "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.21.0" + }, + "time": "2025-01-13T09:32:25+00:00" + }, { "name": "giggsey/libphonenumber-for-php-lite", "version": "8.13.36", @@ -4814,16 +4859,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.41.27", + "version": "0.41.28", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5" + "reference": "8eace11070264c62c8da3c69498fb8dc98fcfaf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/083fd2e8163d6a4e59ee971ac6cb97277d831dd5", - "reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8eace11070264c62c8da3c69498fb8dc98fcfaf7", + "reference": "8eace11070264c62c8da3c69498fb8dc98fcfaf7", "shasum": "" }, "require": { @@ -4859,9 +4904,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.41.27" + "source": "https://github.com/appwrite/sdk-generator/tree/0.41.28" }, - "time": "2025-07-31T10:20:46+00:00" + "time": "2025-08-01T11:06:30+00:00" }, { "name": "doctrine/annotations", @@ -5280,16 +5325,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -5328,7 +5373,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -5336,7 +5381,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", From 7c5dd9d0f4df3b492a495148baf376c113e33d2b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 13:38:59 +0530 Subject: [PATCH 3/8] add test --- tests/e2e/Services/Avatars/AvatarsBase.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index ba66920ed6..915ee4bfa4 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -287,6 +287,16 @@ trait AvatarsBase $this->assertEquals('image/x-icon', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); + $response = $this->client->call(Client::METHOD_GET, '/avatars/favicon', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io/', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/svg+xml', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + /** * Test for FAILURE */ From a00b93416d74aeacd7ba7494663a075cbeee3234 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 13:39:30 +0530 Subject: [PATCH 4/8] fix: cleansvg --- app/controllers/api/avatars.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index b0eaddd742..297b70c1dc 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -480,7 +480,7 @@ App::get('/v1/avatars/favicon') $response ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days ->setContentType('image/svg+xml') - ->file($data); + ->file($cleanSvg); return; } From 4c971910bbc67d324d673febfe937affbd02d17c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 22:08:25 +0530 Subject: [PATCH 5/8] chore: fix import --- app/controllers/api/avatars.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 297b70c1dc..0617a84532 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -10,6 +10,7 @@ use Appwrite\URL\URL as URLParse; use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; +use enshrined\svgSanitize\Sanitizer as SvgSanitizer; use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; @@ -457,7 +458,7 @@ App::get('/v1/avatars/favicon') $data = $res->getBody(); - if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files + if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files if ( empty($data) || stripos($data, 'addHeader('Cache-Control', 'private, max-age=2592000') // 30 days ->setContentType('image/x-icon') ->file($data); + return; } - if ('svg' == $outputExt) { // Skip crop, Imagick isn\'t supporting svg files - $sanitizer = new \Enshrined\SvgSanitize\Sanitizer(); + if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files + $sanitizer = new SvgSanitizer(); $cleanSvg = $sanitizer->sanitize($data); if ($cleanSvg === false) { throw new \Exception('SVG sanitization failed'); From 10b82716e41be9ee32a7e2181b7dde901d979e8e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 3 Aug 2025 22:31:26 +0530 Subject: [PATCH 6/8] chore: update test --- tests/e2e/Services/Avatars/AvatarsBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index 915ee4bfa4..6d34418438 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -284,7 +284,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/x-icon', $response['headers']['content-type']); + $this->assertEquals('image/svg+xml', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/favicon', [ From 048dbe81c7a135b6ac29bc796aaaca9a74151b55 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 Aug 2025 14:34:45 +0530 Subject: [PATCH 7/8] chore: minify svg --- app/controllers/api/avatars.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 0617a84532..d0d77d5c3e 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -475,7 +475,9 @@ App::get('/v1/avatars/favicon') if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files $sanitizer = new SvgSanitizer(); - $cleanSvg = $sanitizer->sanitize($data); + $cleanSvg = $sanitizer + ->minify() + ->sanitize($data); if ($cleanSvg === false) { throw new \Exception('SVG sanitization failed'); } From 7a507917c978c5e2f99925157c27784caeaac50f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 4 Aug 2025 14:41:57 +0530 Subject: [PATCH 8/8] fix: minify --- app/controllers/api/avatars.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index d0d77d5c3e..1d50d2c899 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -475,9 +475,8 @@ App::get('/v1/avatars/favicon') if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files $sanitizer = new SvgSanitizer(); - $cleanSvg = $sanitizer - ->minify() - ->sanitize($data); + $sanitizer->minify(true); + $cleanSvg = $sanitizer->sanitize($data); if ($cleanSvg === false) { throw new \Exception('SVG sanitization failed'); }