mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add new endpoint to retrieve user photos from Gravatar; implement parameter validation and comprehensive tests for various scenarios including output formats and error handling
This commit is contained in:
@@ -863,6 +863,117 @@ App::get('/v1/avatars/screenshots')
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/avatars/photos')
|
||||
->desc('Get user photo from Gravatar')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'avatar/photo')
|
||||
->label('cache.resource', 'photo/{request.userId}/{request.width}/{request.height}/{request.quality}/{request.output}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getPhoto',
|
||||
description: '/docs/references/avatars/get-photo.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
|
||||
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->action(function (int $width, int $height, int $quality, string $output, Response $response, Document $user) {
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$email = $user->getAttribute('email', '');
|
||||
if (empty($email)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND, 'User email not found');
|
||||
}
|
||||
|
||||
// Use the larger of width/height for Gravatar size parameter
|
||||
$gravatarSize = \max($width, $height);
|
||||
$gravatarSize = \max($gravatarSize, 80); // Minimum size for Gravatar
|
||||
|
||||
// Generate Gravatar URL
|
||||
$emailHash = \md5(\strtolower(\trim($email)));
|
||||
$gravatarUrl = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $gravatarSize . '&d=404';
|
||||
|
||||
$domain = new Domain(\parse_url($gravatarUrl, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
try {
|
||||
$res = $client
|
||||
->setAllowRedirects(false)
|
||||
->fetch($gravatarUrl);
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$imageData = null;
|
||||
$isGravatarImage = false;
|
||||
|
||||
if ($res->getStatusCode() === 200) {
|
||||
try {
|
||||
$imageData = $res->getBody();
|
||||
$isGravatarImage = true;
|
||||
} catch (\Throwable $exception) {
|
||||
// Fall through to fallback image
|
||||
}
|
||||
}
|
||||
|
||||
// If no Gravatar image found, use a fallback image
|
||||
if (!$isGravatarImage || empty($imageData)) {
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
$fallbackPath = $fileLogos['default_image'];
|
||||
|
||||
if (!\is_readable($fallbackPath)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Fallback image not readable');
|
||||
}
|
||||
|
||||
$imageData = \file_get_contents($fallbackPath);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = new Image($imageData);
|
||||
} catch (\Throwable $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image');
|
||||
}
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
|
||||
// Determine output format - replicate storage preview logic
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
if (empty($output)) {
|
||||
$output = 'png'; // Default to PNG
|
||||
}
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
unset($image);
|
||||
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['png'];
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType($contentType)
|
||||
->file($data);
|
||||
});
|
||||
|
||||
App::get('/v1/cards/cloud')
|
||||
->desc('Get front Of Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Use this endpoint to fetch a user's photo from Gravatar based on their email address. The API automatically generates the Gravatar URL using the user's email hash and provides standard image cropping options.
|
||||
|
||||
When width and height are specified, the image is resized accordingly. If both dimensions are 0, the API provides an image at original size. If dimensions are not specified, the default size is 100x100px.
|
||||
|
||||
This endpoint requires a valid user email address and will return a 404 if no Gravatar image is found for the user.
|
||||
@@ -1190,4 +1190,87 @@ trait AvatarsBase
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function testGetPhoto(): array
|
||||
{
|
||||
/**
|
||||
* Test for SUCCESS - Note: This test may fail if the test user doesn't have a Gravatar image
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'width' => 200,
|
||||
'height' => 200,
|
||||
]);
|
||||
|
||||
// Gravatar returns 404 if no image is found for the email
|
||||
if ($response['headers']['status-code'] === 404) {
|
||||
$this->markTestSkipped('Test user does not have a Gravatar image');
|
||||
}
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('image/png', $response['headers']['content-type']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
|
||||
// Test with different output format
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'width' => 100,
|
||||
'height' => 100,
|
||||
'quality' => 80,
|
||||
'output' => 'jpeg',
|
||||
]);
|
||||
|
||||
// Gravatar returns 404 if no image is found for the email
|
||||
if ($response['headers']['status-code'] === 404) {
|
||||
$this->markTestSkipped('Test user does not have a Gravatar image');
|
||||
}
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('image/', $response['headers']['content-type']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE - Invalid width parameter
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'width' => 3000, // Too high (max 2000)
|
||||
]);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE - Invalid height parameter
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'height' => 3000, // Too high (max 2000)
|
||||
]);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE - Invalid quality parameter
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'quality' => 150, // Too high (max 100)
|
||||
]);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE - Invalid output parameter
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/avatars/photos', [
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'output' => 'invalid-format', // Invalid format
|
||||
]);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,4 +353,74 @@ class AvatarsTest extends Scope
|
||||
|
||||
return $screenshot['body'];
|
||||
}
|
||||
|
||||
public function testGetPhoto()
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
$query = $this->getQuery(self::GET_PHOTO);
|
||||
$graphQLPayload = [
|
||||
'query' => $query,
|
||||
'variables' => [
|
||||
'width' => 200,
|
||||
'height' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$photo = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), $graphQLPayload);
|
||||
|
||||
$this->assertEquals(200, $photo['headers']['status-code']);
|
||||
$this->assertNotEmpty($photo['body']);
|
||||
|
||||
// Check if GraphQL schema includes the photos field
|
||||
if (!str_contains($photo['headers']['content-type'], 'image/')) {
|
||||
$this->assertArrayHasKey('errors', $photo['body']);
|
||||
$this->assertNotEmpty($photo['body']['errors']);
|
||||
$this->assertStringContainsString('Cannot query field "avatarsGetPhoto"', $photo['body']['errors'][0]['message']);
|
||||
$this->markTestSkipped('GraphQL schema does not include avatarsGetPhoto field yet');
|
||||
}
|
||||
|
||||
$this->assertStringContainsString('image/', $photo['headers']['content-type']);
|
||||
|
||||
return $photo['body'];
|
||||
}
|
||||
|
||||
public function testGetPhotoWithOutput()
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
$query = $this->getQuery(self::GET_PHOTO);
|
||||
$graphQLPayload = [
|
||||
'query' => $query,
|
||||
'variables' => [
|
||||
'width' => 100,
|
||||
'height' => 100,
|
||||
'quality' => 80,
|
||||
'output' => 'jpeg',
|
||||
],
|
||||
];
|
||||
|
||||
$photo = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), $graphQLPayload);
|
||||
|
||||
$this->assertEquals(200, $photo['headers']['status-code']);
|
||||
$this->assertNotEmpty($photo['body']);
|
||||
|
||||
// Check if GraphQL schema includes the photos field
|
||||
if (!str_contains($photo['headers']['content-type'], 'image/')) {
|
||||
$this->assertArrayHasKey('errors', $photo['body']);
|
||||
$this->assertNotEmpty($photo['body']['errors']);
|
||||
$this->assertStringContainsString('Cannot query field "avatarsGetPhoto"', $photo['body']['errors'][0]['message']);
|
||||
$this->markTestSkipped('GraphQL schema does not include avatarsGetPhoto field yet');
|
||||
}
|
||||
|
||||
$this->assertStringContainsString('image/', $photo['headers']['content-type']);
|
||||
|
||||
return $photo['body'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,7 @@ trait Base
|
||||
public const string GET_QRCODE = 'get_qrcode';
|
||||
public const string GET_USER_INITIALS = 'get_user_initials';
|
||||
public const string GET_SCREENSHOT = 'get_screenshot';
|
||||
public const string GET_PHOTO = 'get_photo';
|
||||
|
||||
// Providers
|
||||
public const string CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
|
||||
@@ -1786,6 +1787,12 @@ trait Base
|
||||
status
|
||||
}
|
||||
}';
|
||||
case self::GET_PHOTO:
|
||||
return 'query getPhoto($width: Int, $height: Int, $quality: Int, $output: String) {
|
||||
avatarsGetPhoto(width: $width, height: $height, quality: $quality, output: $output) {
|
||||
status
|
||||
}
|
||||
}';
|
||||
case self::GET_ACCOUNT:
|
||||
return 'query getAccount {
|
||||
accountGet {
|
||||
|
||||
Reference in New Issue
Block a user