Skip to content

Avatars module#11103

Merged
abnegate merged 9 commits into1.8.xfrom
avatars-module
Jan 12, 2026
Merged

Avatars module#11103
abnegate merged 9 commits into1.8.xfrom
avatars-module

Conversation

@ItzNotABug
Copy link
Copy Markdown
Contributor

What does this PR do?

(Provide a description of what this PR does and why it's needed.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@ItzNotABug ItzNotABug self-assigned this Jan 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

The Avatars functionality was moved from a single controller (app/controllers/api/avatars.php removed) into a new Avatars platform module. The platform now registers Appwrite\Platform\Modules\Avatars\Module and its HTTP service, and many new HTTP action classes were added (browsers, credit-cards, flags, image, favicon, qr, initials, screenshots, cards cloud/front/back/og) plus shared Action utilities for image processing, OAuth, and caching. Service config in app/config/services.php was updated to indicate avatars now use modules.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains only the repository's PR template with placeholder prompts and no actual details about the changes, motivation, test plan, or related issues. Replace the template placeholders with concrete information: describe what the PR does (module refactoring), explain the motivation, provide a test plan, and link related PRs/issues.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Avatars module' directly and concisely describes the main change: refactoring avatars endpoints from a controller-based API file into a modular platform architecture.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 384c47f and 4eb1e7a.

📒 Files selected for processing (1)
  • app/controllers/api/avatars.php
💤 Files with no reviewable changes (1)
  • app/controllers/api/avatars.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Benchmark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 7, 2026

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
libpng 1.6.51-r0 CVE-2025-66293 HIGH
libpng-dev 1.6.51-r0 CVE-2025-66293 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 7, 2026

✨ Benchmark results

  • Requests per second: 2,171
  • Requests with 200 status code: 390,821
  • P99 latency: 0.081316735

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 2,171 1,205
200 390,821 216,996
P99 0.081316735 0.170349549

@ItzNotABug ItzNotABug marked this pull request as ready for review January 7, 2026 14:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In @app/config/services.php:
- Line 51: Delete the obsolete avatars controller file named avatars.php (the
old API avatars controller) from the codebase to complete the migration; remove
any remaining references to it (routes, service config entries, autoload lists)
and run a quick search or test suite to confirm nothing still imports or
instantiates the avatars controller.

In @src/Appwrite/Platform/Modules/Avatars/Http/Action.php:
- Around line 106-108: Fix the typo in the inline comment next to the check for
$verificationId in Action.php: change "Race codition, handeled in catch" to
"Race condition, handled in catch" near the if (empty($verificationId)) { throw
new \Exception("Locked tokens."); } block so the comment reads correctly and
references the catch handling for the race condition.

In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php:
- Line 45: The parameter description for the 'width' param in the Get.php
avatars card has a double space ("Resize  image width"); update the string
passed to the ->param call for 'width' (the description argument in the
builder/validator invocation) to use a single space so it reads "Resize image
width, Pass an integer between 0 to 512.".
- Around line 95-96: Remove the unnecessary setlocale(LC_ALL, "en_US.utf8");
call and delete the commented-out iconv line (// $userId = \iconv("utf-8",
"ascii//TRANSLIT", $userId);) in the Get.php cloud avatars handler so no dead
code or unintended locale side-effects remain; locate these statements near the
top of the Get class/method handling the cloud back card response and simply
remove both lines, leaving any real userId normalization logic intact if present
elsewhere.

In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php:
- Around line 195-197: The truncation of $name in Get.php uses strlen() and
substr(), which can split multibyte UTF‑8 characters; replace the checks and
trimming around the $name variable (the block that currently uses \strlen($name)
and \substr($name, 0, 32)) with mb_strlen() and mb_substr() using the 'UTF-8'
encoding (or default internal encoding) so the string is measured and sliced by
characters not bytes; ensure mbstring is available or fall back safely if
needed.

In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php:
- Around line 294-296: The code truncates UTF-8 names using \strlen and \substr
which can break multibyte characters; in the Get class/method handling OG Cards
(file Get.php) replace \strlen($name) and \substr($name, ...) with
\mb_strlen($name) and \mb_substr($name, ...), and update the other length checks
that compare $name length (the subsequent conditional/switch checks in the same
method) to use \mb_strlen($name) as well so truncation and comparisons are UTF-8
safe.

In @src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php:
- Around line 160-168: The second HTTP client call using new Client() (variable
$client) before ->fetch($outputHref) is missing the same User-Agent header used
earlier; update the chain to include ->setUserAgent(...) with the same value or
variable used in the first fetch (so the request uses a proper User-Agent),
keeping the existing
->setAllowRedirects(true)->setMaxRedirects(5)->fetch($outputHref) flow and
preserving the existing Exception::AVATAR_REMOTE_URL_FAILED behavior.
- Line 110: The current merge of parse_url($url) and parse_url($href) can
produce invalid results when $href is already absolute; update the logic in
Get.php so you first detect if $href is absolute (e.g.,
parse_url($href)['scheme'] is set or $href starts with '//') and if so use $href
(or URLParse::unparse(parse_url($href))) directly as $absolute; only when $href
is relative perform URLParse::unparse(array_merge(parse_url($url),
parse_url($href))). Keep references to URLParse::unparse, parse_url, and the
$url/$href variables when making this change.

In @src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php:
- Around line 78-85: The fetch currently disables redirects via the Client call
chain ("$client = new Client();" and "->setAllowRedirects(false)->fetch($url)"),
which will break legitimate redirected image URLs; change the configuration to
allow redirects (use the same pattern as the Favicon endpoint by passing an
allowance for up to 5 redirects, e.g. replace setAllowRedirects(false) with
setAllowRedirects(5) or an equivalent that enables up to 5 redirects) so remote
image fetching follows redirects before throwing
Exception::AVATAR_REMOTE_URL_FAILED.

In @src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php:
- Around line 56-57: The param descriptions for the 'width' and 'height'
parameters are incorrect: they pass default value 500 but say "Defaults to 100."
Update the description strings for ->param('width', 500, new Range(0, 2000),
...) and ->param('height', 500, new Range(0, 2000), ...) to state "Defaults to
500." so the documentation matches the actual default values.

In @src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php:
- Around line 80-83: The QR avatar endpoint currently uses $response->send(...)
which differs from other avatar endpoints; replace the use of ->send() with
->file() so the response uses $response->file($image->output('png', 90)) and
keep the existing headers (Cache-Control and setContentType) unchanged to match
the pattern used by Initials/Image/Favicon/Screenshots/Cards endpoints.

In @src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php:
- Around line 221-223: The catch block in Get.php currently swallows the
original exception by always throwing new
Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: '
. $th->getMessage()), so preserve the original exception context: if $th is
already an Exception with a specific code (e.g.,
Exception::AVATAR_IMAGE_NOT_FOUND) rethrow $th; otherwise throw the
AVATAR_REMOTE_URL_FAILED but pass $th as the previous exception (third
constructor arg) so the original stack/type is retained; update the catch in the
method that contains this try/catch to implement this logic.
- Line 75: Update the timezone example to use the correct IANA casing: in the
param definition for 'timezone' (the ->param('timezone', '', new
WhiteList(timezone_identifiers_list()), ...) call in Get.php) replace the
example value 'america/new_york' with 'America/New_York' so it matches the
identifiers returned by timezone_identifiers_list() and the WhiteList validator.
🧹 Nitpick comments (14)
src/Appwrite/Platform/Modules/Avatars/Services/Http.php (1)

7-7: Consider more explicit naming for cloud card variants.

The import alias GetCloudCard for Cards\Cloud\Front\Get is implicit about being the "Front" variant, while GetCloudCardBack and GetCloudCardOG are explicit. Consider renaming to GetCloudCardFront for consistency.

♻️ Proposed naming improvement
-use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front\Get as GetCloudCard;
+use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front\Get as GetCloudCardFront;
-        $this->addAction(GetCloudCard::getName(), new GetCloudCard());
+        $this->addAction(GetCloudCardFront::getName(), new GetCloudCardFront());

Also applies to: 32-32

src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php (2)

33-42: Missing cache label.

Other avatar endpoints (e.g., CreditCards, Browsers, Favicon) include ->label('cache', true). This endpoint only has cache.resource but is missing the primary cache label, which may cause inconsistent caching behavior.

📝 Proposed fix
             ->groups(['api', 'avatars'])
             ->label('scope', 'avatars.read')
+            ->label('cache', true)
             ->label('cache.resource', 'avatar/initials')

99-125: Consider explicitly destroying Imagick objects.

The $image, $punch, and $draw Imagick objects are not explicitly destroyed. While PHP's garbage collector will handle this, explicit cleanup via destroy() is recommended for memory-intensive image operations.

♻️ Proposed fix
+        $imageBlob = $image->getImageBlob();
+
+        $draw->clear();
+        $punch->destroy();
+        $image->destroy();
+
         $response
             ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
             ->setContentType('image/png')
-            ->file($image->getImageBlob());
+            ->file($imageBlob);
     }
src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php (1)

176-188: ICO validation heuristic is fragile.

The check stripos($data, '<html') === 0 || stripos($data, '<!doc') === 0 only detects HTML at the very start of the response body. A valid ICO file could theoretically start with bytes that match these patterns, or an error page might have leading whitespace.

Consider checking the response Content-Type header as a primary indicator before falling back to content inspection:

+        $contentType = $res->getHeader('Content-Type')[0] ?? '';
+        $isHtmlResponse = \str_contains($contentType, 'text/html');
+
         if ('ico' === $outputExt) {
             if (
                 empty($data) ||
+                $isHtmlResponse ||
                 stripos($data, '<html') === 0 ||
                 stripos($data, '<!doc') === 0
             ) {
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php (1)

47-49: Injected $user parameter is immediately overwritten.

The $user is injected via ->inject('user') but the action immediately overwrites it with a fresh database query. Either remove the injection or utilize the injected user when $userId matches.

♻️ Proposed fix - remove unused injection
             ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
-            ->inject('user')
             ->inject('project')
             ->inject('dbForProject')

And update the action signature:

-    public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger)
+    public function action(string $userId, string $mock, int $width, int $height, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger)

Also applies to: 59-61

src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php (1)

64-66: Remove redundant variable assignments.

The $output and $type variables are both always 'png', and the conditional (empty($output)) ? $type : $output on line 98 will never trigger. This is dead code.

♻️ Proposed fix
     public function action(string $url, int $width, int $height, Response $response)
     {
         $quality = 80;
-        $output = 'png';
-        $type = 'png';

         // ... existing code ...

         $image->crop((int) $width, (int) $height);
-        $output = (empty($output)) ? $type : $output;
-        $data = $image->output($output, $quality);
+        $data = $image->output('png', $quality);

Also applies to: 97-98

src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php (1)

36-52: Missing cache and cache.resource labels.

Unlike other avatar endpoints (Browsers, CreditCards, Favicon, Image), this endpoint doesn't have the cache and cache.resource labels, which may lead to inconsistent caching behavior for QR codes.

📝 Proposed fix
             ->groups(['api', 'avatars'])
             ->label('scope', 'avatars.read')
+            ->label('cache', true)
+            ->label('cache.resource', 'avatar/qr')
             ->label('sdk', new Method(
src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php (1)

154-161: Geolocation at (0, 0) is a valid coordinate but will be ignored.

The condition $latitude != 0 || $longitude != 0 excludes the valid coordinate (0, 0) in the Gulf of Guinea. While this is an edge case, consider using a separate flag or null defaults to distinguish "not provided" from "explicitly set to zero."

src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)

61-61: Unused $logger parameter.

The $logger parameter is declared but never used in getUserGitHub. Either remove it or add logging for OAuth token refresh failures and race condition retries to aid debugging.


42-53: Dead code: $output reassignment is a no-op.

$output is set to 'png' on line 42, so the condition empty($output) on line 52 is always false. The $type variable (line 44) is also unused after being set.

Proposed cleanup
-        $output = 'png';
         $path = $set[$code]['path'];
-        $type = 'png';

         if (!\is_readable($path)) {
             throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
         }

         $image = new Image(\file_get_contents($path));
         $image->crop((int) $width, (int) $height);
-        $output = (empty($output)) ? $type : $output;
-        $data = $image->output($output, $quality);
+        $data = $image->output('png', $quality);
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php (1)

59-61: Injected $user parameter is immediately overwritten.

The $user Document is injected via ->inject('user') at line 47 but immediately replaced at line 61 with a fresh database fetch using $userId. Either remove the injection or use the injected user when $userId is empty.

src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php (3)

59-61: Injected $user parameter is immediately overwritten.

Same issue as in Front/Get.php: the $user Document is injected but replaced at line 61 with a fresh database fetch.


67-100: Significant code duplication with Front/Get.php.

The role determination logic (isHero, isContributor, isEmployee, isPlatinum), GitHub fetching, memberSince calculation, and mutual exclusion normalization (lines 123-140) are nearly identical to Front/Get.php. Consider extracting this common logic to a shared method in the base Action class to improve maintainability.

Also applies to: 123-140


284-287: Commented-out iconv lines should be removed or explained.

The commented-out transliteration code appears in both this file and Front/Get.php. If it's no longer needed, remove it; if it's being preserved for future use, add a TODO comment explaining why.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ded06d7 and 384c47f.

📒 Files selected for processing (16)
  • app/config/services.php
  • src/Appwrite/Platform/Appwrite.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Action.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php
  • src/Appwrite/Platform/Modules/Avatars/Module.php
  • src/Appwrite/Platform/Modules/Avatars/Services/Http.php
🧰 Additional context used
🧬 Code graph analysis (13)
src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php (1)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)
  • Action (17-158)
  • avatarCallback (24-59)
src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php (1)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)
  • Action (17-158)
  • avatarCallback (24-59)
src/Appwrite/Platform/Modules/Avatars/Module.php (2)
src/Appwrite/Platform/Appwrite.php (1)
  • Appwrite (18-34)
src/Appwrite/Platform/Modules/Avatars/Services/Http.php (1)
  • Http (18-36)
src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php (1)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)
  • Action (17-158)
  • avatarCallback (24-59)
src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php (2)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (1)
  • Action (17-158)
src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php (4)
  • Get (21-107)
  • getName (25-28)
  • __construct (30-60)
  • action (62-106)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php (2)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (3)
  • Action (17-158)
  • getUserGitHub (61-157)
  • getAppRoot (19-22)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php (4)
  • Get (21-244)
  • getName (25-28)
  • __construct (30-57)
  • action (59-243)
src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php (1)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)
  • Action (17-158)
  • getAppRoot (19-22)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php (3)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (3)
  • Action (17-158)
  • getUserGitHub (61-157)
  • getAppRoot (19-22)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php (4)
  • Get (21-115)
  • getName (25-28)
  • __construct (30-57)
  • action (59-114)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php (4)
  • Get (21-244)
  • getName (25-28)
  • __construct (30-57)
  • action (59-243)
src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php (5)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (1)
  • Action (17-158)
src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php (4)
  • Get (22-127)
  • getName (26-29)
  • __construct (31-62)
  • action (64-126)
src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php (4)
  • Get (18-64)
  • getName (22-25)
  • __construct (27-58)
  • action (60-63)
src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php (4)
  • Get (25-216)
  • getName (29-32)
  • __construct (34-62)
  • action (64-215)
src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php (4)
  • Get (21-85)
  • getName (25-28)
  • __construct (30-59)
  • action (61-84)
src/Appwrite/Platform/Appwrite.php (2)
src/Appwrite/Platform/Modules/Avatars/Module.php (1)
  • Module (8-14)
src/Appwrite/Platform/Modules/Account/Module.php (1)
  • Module (8-14)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (1)
src/Appwrite/Extend/Exception.php (1)
  • Exception (7-472)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php (2)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (3)
  • Action (17-158)
  • getUserGitHub (61-157)
  • getAppRoot (19-22)
src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php (4)
  • Get (21-115)
  • getName (25-28)
  • __construct (30-57)
  • action (59-114)
src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php (2)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php (1)
  • Action (17-158)
src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php (4)
  • Get (21-107)
  • getName (25-28)
  • __construct (30-60)
  • action (62-106)
🪛 PHPMD (2.15.0)
src/Appwrite/Platform/Modules/Avatars/Http/Action.php

61-61: Avoid unused parameters such as '$logger'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: E2E Service Test (Databases/Legacy)
  • GitHub Check: E2E Service Test (Databases/TablesDB)
🔇 Additional comments (20)
src/Appwrite/Platform/Appwrite.php (1)

6-6: LGTM! Module registration follows the established pattern.

The Avatars module import and registration are consistent with other modules in the platform. The placement maintains alphabetical ordering in the constructor.

Also applies to: 24-24

app/config/services.php (1)

149-149: Documentation comment added for storage service.

Good practice to document that the storage service uses module-based routing.

src/Appwrite/Platform/Modules/Avatars/Module.php (1)

8-13: LGTM! Module implementation is clean and correct.

The Avatars module follows the exact same pattern as other modules (e.g., Account). The HTTP service registration is straightforward and properly structured.

src/Appwrite/Platform/Modules/Avatars/Http/Flags/Get.php (3)

27-58: LGTM! Flag endpoint implementation follows established patterns.

The endpoint configuration is consistent with other avatar endpoints (e.g., browsers). The validation rules, caching labels, and SDK configuration are properly structured.


60-63: Action base class properly provides avatarCallback method.

The Action base class exists at src/Appwrite/Platform/Modules/Avatars/Http/Action.php and correctly implements the avatarCallback method with proper error handling for missing avatar sets, invalid codes, and required extensions.


52-52: No action required. The avatar-flags configuration is properly defined in app/config/avatars/flags.php, loaded during app initialization in app/init/configs.php, and contains the expected ISO Alpha-2 country code mappings with consistent structure (name and path properties). The WhiteList validator will function correctly.

src/Appwrite/Platform/Modules/Avatars/Services/Http.php (1)

20-35: All avatar action classes are implemented and properly configured. All 11 referenced classes exist at their expected paths and are correctly imported with appropriate aliases in the Http.php service file.

src/Appwrite/Platform/Modules/Avatars/Http/CreditCards/Get.php (1)

18-64: LGTM!

Clean implementation following the established avatar endpoint pattern. The action correctly delegates to avatarCallback with appropriate parameters.

src/Appwrite/Platform/Modules/Avatars/Http/Browsers/Get.php (1)

18-64: LGTM!

Clean implementation that correctly delegates to avatarCallback following the established pattern for static avatar assets.

src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php (1)

61-63: Do not remove the type coercion for $download.

The manual coercion on line 63 is necessary, not redundant. The Utopia framework validates the Boolean parameter but does not type-cast it to a native bool. Query string parameters like download=0 remain as the string '0' after validation, which is truthy in PHP (non-empty string). Without the manual coercion, if ($download) would evaluate to true even when the user passes download=0.

The same coercion pattern appears in the legacy code at app/controllers/api/avatars.php:527, confirming this is the intended design to handle loose boolean values from query parameters correctly.

Likely an incorrect or invalid review comment.

src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php (3)

91-95: LGTM on the Imagick availability check.

The extension check is appropriately placed at the start of the action, consistent with other avatar endpoints in the module.


103-104: Timeout configuration is appropriate.

30-second timeout for browser service calls is reasonable for screenshot generation.


191-207: Image processing logic is well-structured.

The conditional processing based on width/height/quality/output parameters avoids unnecessary image manipulation when defaults are used.

src/Appwrite/Platform/Modules/Avatars/Http/Action.php (2)

154-156: Exception handling only catches Appwrite\Extend\Exception.

The catch block on line 154 only catches the Appwrite-specific Exception class. Other exceptions (e.g., from OAuth provider, database operations) will propagate uncaught, potentially exposing implementation details to callers. Consider catching \Throwable if the intent is to gracefully degrade to an empty array for all failures.


118-143: Race condition retry loop is well-designed.

The retry mechanism with 500ms sleep intervals (up to 10 retries = 5 seconds max) to handle concurrent token refresh is a reasonable approach. The break condition checking for token change is correct.

src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php (3)

81-93: DateTime constructor with empty string uses current time.

When $heroes[$email]['memberSince'] or $employees[$email]['memberSince'] is missing or empty, new \DateTime('') defaults to the current time rather than throwing. This may be intentional but could lead to confusing "member since" dates if data is incomplete.


113-126: Role mutual exclusion logic is correct but verbose.

The cascading if blocks ensure only one role is active. The logic works correctly: employee takes precedence, then hero, then contributor. Consider simplifying if readability is a concern.


235-237: Resize only when width OR height is non-zero may distort image.

When only $width or only $height is provided (but not both), resizeImage() with the other dimension as 0 may produce unexpected results depending on Imagick's behavior. Verify that this produces the intended scaling.

src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/OG/Get.php (2)

110-118: employeeNumber match may not cover all employee mock variants.

The match statement handles specific employee mocks but uses default => '' for others. If a mock value like 'employee-bg2' were added to the whitelist, it would match str_starts_with($mock, 'employee') at line 109 but get an empty employee number. This may be intentional but could cause subtle bugs if the whitelist grows.


162-246: Employee badge rendering logic is thorough.

The per-variation positioning, rotation, and skew transformations for employee badges are well-implemented with appropriate handling of single vs. double-digit employee numbers.

Comment on lines +106 to +108
if (empty($verificationId)) {
throw new \Exception("Locked tokens."); // Race codition, handeled in catch
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typos in comment.

"Race codition, handeled in catch" should be "Race condition, handled in catch".

Proposed fix
-                    if (empty($verificationId)) {
-                        throw new \Exception("Locked tokens."); // Race codition, handeled in catch
-                    }
+                    if (empty($verificationId)) {
+                        throw new \Exception("Locked tokens."); // Race condition, handled in catch
+                    }
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Action.php around lines 106 -
108, Fix the typo in the inline comment next to the check for $verificationId in
Action.php: change "Race codition, handeled in catch" to "Race condition,
handled in catch" near the if (empty($verificationId)) { throw new
\Exception("Locked tokens."); } block so the comment reads correctly and
references the catch handling for the race condition.

->label('origin', '*')
->param('userId', '', new UID(), 'User ID.', true)
->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true)
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor typo: double space in description.

"Resize image width" has an extra space.

📝 Proposed fix
-            ->param('width', 0, new Range(0, 512), 'Resize  image width, Pass an integer between 0 to 512.', true)
+            ->param('width', 0, new Range(0, 512), 'Resize image width. Pass an integer between 0 to 512.', true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
->param('width', 0, new Range(0, 512), 'Resize image width. Pass an integer between 0 to 512.', true)
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php at line
45, The parameter description for the 'width' param in the Get.php avatars card
has a double space ("Resize  image width"); update the string passed to the
->param call for 'width' (the description argument in the builder/validator
invocation) to use a single space so it reads "Resize image width, Pass an
integer between 0 to 512.".

Comment on lines +95 to +96
setlocale(LC_ALL, "en_US.utf8");
// $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove dead code and unused setlocale call.

The setlocale call appears unnecessary, and the commented-out iconv line is dead code that should be removed.

🧹 Proposed fix
-        setlocale(LC_ALL, "en_US.utf8");
-        // $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
-
         $text = new ImagickDraw();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setlocale(LC_ALL, "en_US.utf8");
// $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
$text = new ImagickDraw();
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Back/Get.php around
lines 95 - 96, Remove the unnecessary setlocale(LC_ALL, "en_US.utf8"); call and
delete the commented-out iconv line (// $userId = \iconv("utf-8",
"ascii//TRANSLIT", $userId);) in the Get.php cloud avatars handler so no dead
code or unintended locale side-effects remain; locate these statements near the
top of the Get class/method handling the cloud back card response and simply
remove both lines, leaving any real userId normalization logic intact if present
elsewhere.

Comment on lines +195 to +197
if (\strlen($name) > 32) {
$name = \substr($name, 0, 32);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

UTF-8 string truncation may corrupt multibyte characters.

Using \strlen() and \substr() on UTF-8 strings can split multibyte characters. Consider using mb_strlen() and mb_substr() for safe Unicode handling.

Proposed fix
-        if (\strlen($name) > 32) {
-            $name = \substr($name, 0, 32);
+        if (\mb_strlen($name) > 32) {
+            $name = \mb_substr($name, 0, 32);
         }

-        if (\strlen($name) <= 23) {
+        if (\mb_strlen($name) <= 23) {
             $text->setFontSize(80);
             $scalingDown = false;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Cards/Cloud/Front/Get.php around
lines 195 - 197, The truncation of $name in Get.php uses strlen() and substr(),
which can split multibyte UTF‑8 characters; replace the checks and trimming
around the $name variable (the block that currently uses \strlen($name) and
\substr($name, 0, 32)) with mb_strlen() and mb_substr() using the 'UTF-8'
encoding (or default internal encoding) so the string is measured and sliced by
characters not bytes; ensure mbstring is available or fall back safely if
needed.

Comment on lines +78 to +85
$client = new Client();
try {
$res = $client
->setAllowRedirects(false)
->fetch($url);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider allowing redirects for remote image fetching.

The client is configured with setAllowRedirects(false), but many image hosting services and CDNs use redirects. This could cause legitimate image URLs to fail. The Favicon endpoint allows up to 5 redirects for similar remote fetching.

📝 Proposed fix
         $client = new Client();
         try {
             $res = $client
-                ->setAllowRedirects(false)
+                ->setAllowRedirects(true)
+                ->setMaxRedirects(5)
                 ->fetch($url);
         } catch (\Throwable) {
             throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$client = new Client();
try {
$res = $client
->setAllowRedirects(false)
->fetch($url);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$client = new Client();
try {
$res = $client
->setAllowRedirects(true)
->setMaxRedirects(5)
->fetch($url);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php around lines 78 -
85, The fetch currently disables redirects via the Client call chain ("$client =
new Client();" and "->setAllowRedirects(false)->fetch($url)"), which will break
legitimate redirected image URLs; change the configuration to allow redirects
(use the same pattern as the Favicon endpoint by passing an allowance for up to
5 redirects, e.g. replace setAllowRedirects(false) with setAllowRedirects(5) or
an equivalent that enables up to 5 redirects) so remote image fetching follows
redirects before throwing Exception::AVATAR_REMOTE_URL_FAILED.

Comment on lines +56 to +57
->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation mismatch: default values don't match descriptions.

The width and height parameters default to 500, but the descriptions state "Defaults to 100." Update the descriptions to reflect the actual defaults.

📝 Proposed fix
-            ->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
-            ->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
+            ->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 500.', true)
+            ->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 500.', true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 500.', true)
->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 500.', true)
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php around lines 56
- 57, The param descriptions for the 'width' and 'height' parameters are
incorrect: they pass default value 500 but say "Defaults to 100." Update the
description strings for ->param('width', 500, new Range(0, 2000), ...) and
->param('height', 500, new Range(0, 2000), ...) to state "Defaults to 500." so
the documentation matches the actual default values.

Comment on lines +80 to +83
$response
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->setContentType('image/png')
->send($image->output('png', 90));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other avatar endpoints output their responses
rg -n '->file\(|->send\(' src/Appwrite/Platform/Modules/Avatars/Http/ --type=php

Repository: appwrite/appwrite

Length of output: 465


🏁 Script executed:

#!/bin/bash
# Check how other avatar endpoints output their responses
rg '\->file\(|\->send\(' src/Appwrite/Platform/Modules/Avatars/Http/ --type=php -n

Repository: appwrite/appwrite

Length of output: 1196


🏁 Script executed:

#!/bin/bash
# Get the QR endpoint code around the response
sed -n '75,85p' src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php

Repository: appwrite/appwrite

Length of output: 367


🏁 Script executed:

#!/bin/bash
# Check the Initials endpoint
sed -n '120,130p' src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php

echo "---"

# Check the Image endpoint
sed -n '99,109p' src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php

Repository: appwrite/appwrite

Length of output: 585


Use file() instead of send() for consistency with all other avatar endpoints.

The QR endpoint is the only avatar endpoint using ->send() to output image data; all other endpoints (Initials, Image, Favicon, Screenshots, Cards) use ->file(). Notably, the Image endpoint uses ->file($image->output(...)) with the exact same data type. Change line 83 to use ->file($image->output('png', 90)) to match the standard pattern.

🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/QR/Get.php around lines 80 - 83,
The QR avatar endpoint currently uses $response->send(...) which differs from
other avatar endpoints; replace the use of ->send() with ->file() so the
response uses $response->file($image->output('png', 90)) and keep the existing
headers (Cache-Control and setContentType) unchanged to match the pattern used
by Initials/Image/Favicon/Screenshots/Cards endpoints.

->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15')
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true')
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Timezone example uses incorrect casing.

The example 'america/new_york' should be 'America/New_York' to match the IANA timezone identifier format. The WhiteList validator uses timezone_identifiers_list() which returns properly cased identifiers.

Proposed fix
-            ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
+            ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York')
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php at line 75,
Update the timezone example to use the correct IANA casing: in the param
definition for 'timezone' (the ->param('timezone', '', new
WhiteList(timezone_identifiers_list()), ...) call in Get.php) replace the
example value 'america/new_york' with 'America/New_York' so it matches the
identifiers returned by timezone_identifiers_list() and the WhiteList validator.

Comment on lines +221 to +223
} catch (\Throwable $th) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Original exception context is swallowed.

All exceptions thrown within the try block are caught and re-thrown as AVATAR_REMOTE_URL_FAILED, losing the original exception type (including the more specific AVATAR_IMAGE_NOT_FOUND from line 188). Consider preserving specific exception types or at least passing the original exception as a previous exception.

Proposed fix
-        } catch (\Throwable $th) {
-            throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
+        } catch (Exception $e) {
+            throw $e; // Re-throw Appwrite exceptions as-is
+        } catch (\Throwable $th) {
+            throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (\Throwable $th) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
}
} catch (Exception $e) {
throw $e; // Re-throw Appwrite exceptions as-is
} catch (\Throwable $th) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
}
🤖 Prompt for AI Agents
In @src/Appwrite/Platform/Modules/Avatars/Http/Screenshots/Get.php around lines
221 - 223, The catch block in Get.php currently swallows the original exception
by always throwing new Exception(Exception::AVATAR_REMOTE_URL_FAILED,
'Screenshot generation failed: ' . $th->getMessage()), so preserve the original
exception context: if $th is already an Exception with a specific code (e.g.,
Exception::AVATAR_IMAGE_NOT_FOUND) rethrow $th; otherwise throw the
AVATAR_REMOTE_URL_FAILED but pass $th as the previous exception (third
constructor arg) so the original stack/type is retained; update the catch in the
method that contains this try/catch to implement this logic.

Copy link
Copy Markdown
Member

@abnegate abnegate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, let's make sure tests still pass with the old controller removed

@abnegate abnegate merged commit 15922fb into 1.8.x Jan 12, 2026
41 checks passed
@abnegate abnegate deleted the avatars-module branch January 12, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants