Merge pull request #10255 from appwrite/chore-support-svg-favicon

chore: add support for svg favicons
This commit is contained in:
Luke B. Silver 2025-08-04 10:42:38 +01:00 committed by GitHub
commit d28e0152fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 91 additions and 7 deletions

View file

@ -10,6 +10,7 @@ use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
use Utopia\App; use Utopia\App;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Database\Database; use Utopia\Database\Database;
@ -362,7 +363,8 @@ App::get('/v1/avatars/favicon')
$client = new Client(); $client = new Client();
try { try {
$res = $client $res = $client
->setAllowRedirects(false) ->setAllowRedirects(true)
->setMaxRedirects(5)
->setUserAgent(\sprintf( ->setUserAgent(\sprintf(
APP_USERAGENT, APP_USERAGENT,
System::getEnv('_APP_VERSION', 'UNKNOWN'), System::getEnv('_APP_VERSION', 'UNKNOWN'),
@ -399,6 +401,12 @@ App::get('/v1/avatars/favicon')
$ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION);
switch ($ext) { switch ($ext) {
case 'svg':
// SVG icons are prioritized by assigning the maximum possible value.
$space = PHP_INT_MAX;
$outputHref = $absolute;
$outputExt = $ext;
break;
case 'ico': case 'ico':
case 'png': case 'png':
case 'jpg': case 'jpg':
@ -437,7 +445,8 @@ App::get('/v1/avatars/favicon')
$client = new Client(); $client = new Client();
try { try {
$res = $client $res = $client
->setAllowRedirects(false) ->setAllowRedirects(true)
->setMaxRedirects(5)
->fetch($outputHref); ->fetch($outputHref);
} catch (\Throwable) { } catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
@ -449,14 +458,33 @@ App::get('/v1/avatars/favicon')
$data = $res->getBody(); $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) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') { if (
empty($data) ||
stripos($data, '<html') === 0 ||
stripos($data, '<!doc') === 0
) {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found'); throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
} }
$response $response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/x-icon') ->setContentType('image/x-icon')
->file($data); ->file($data);
return;
}
if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files
$sanitizer = new SvgSanitizer();
$sanitizer->minify(true);
$cleanSvg = $sanitizer->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')
->file($cleanSvg);
return;
} }
$image = new Image($data); $image = new Image($data);

View file

@ -82,7 +82,8 @@
"adhocore/jwt": "1.1.*", "adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "^10.0", "spomky-labs/otphp": "^10.0",
"webonyx/graphql-php": "14.11.*", "webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*" "league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.21.*"
}, },
"require-dev": { "require-dev": {
"ext-fileinfo": "*", "ext-fileinfo": "*",

47
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "edbe5912c45e1f467f398541a75a77de", "content-hash": "7b2ef6192403daf5c492219822ce0aa1",
"packages": [ "packages": [
{ {
"name": "adhocore/jwt", "name": "adhocore/jwt",
@ -628,6 +628,51 @@
], ],
"time": "2023-08-10T19:36:49+00:00" "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", "name": "giggsey/libphonenumber-for-php-lite",
"version": "8.13.36", "version": "8.13.36",

View file

@ -284,7 +284,17 @@ trait AvatarsBase
]); ]);
$this->assertEquals(200, $response['headers']['status-code']); $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', [
'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']); $this->assertNotEmpty($response['body']);
/** /**