Add display picture support and error code refactor

This commit is contained in:
netkas 2024-12-24 15:05:35 -05:00
parent 85bdff7d3c
commit 738f8a455c
7 changed files with 523 additions and 286 deletions

View file

@ -37,7 +37,7 @@
*/ */
public function getUserDisplayImagesPath(): string public function getUserDisplayImagesPath(): string
{ {
return $this->userDisplayImagesPath; return $this->path . DIRECTORY_SEPARATOR . $this->userDisplayImagesPath;
} }
/** /**

View file

@ -0,0 +1,69 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Utilities;
use Socialbox\Enums\Flags\SessionFlags;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\RegisteredPeerManager;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class SettingsSetDisplayPicture extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('image'))
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'image' parameter");
}
if(strlen($rpcRequest->getParameter('image')) > Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize())
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Image size exceeds the maximum allowed size of " . Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize() . " bytes");
}
try
{
$decodedImage = base64_decode($rpcRequest->getParameter('image'));
if($decodedImage === false)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, "Failed to decode JPEG image base64 data");
}
$sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126);
}
catch(Exception $e)
{
throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::BAD_REQUEST, $e);
}
try
{
// Set the password
RegisteredPeerManager::updateDisplayPicture($request->getPeer(), $sanitizedImage);
// Remove the SET_DISPLAY_PICTURE flag
SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_DISPLAY_PICTURE]);
// Check & update the session flow
SessionManager::updateFlow($request->getSession());
}
catch(Exception $e)
{
throw new StandardException('Failed to update display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -1,15 +1,16 @@
<?php <?php
namespace Socialbox\Classes; namespace Socialbox\Classes;
use InvalidArgumentException; use Exception;
use JsonException; use InvalidArgumentException;
use RuntimeException; use JsonException;
use Socialbox\Enums\StandardHeaders; use RuntimeException;
use Throwable; use Socialbox\Enums\StandardHeaders;
use Throwable;
class Utilities class Utilities
{ {
/** /**
* Decodes a JSON string into an associative array, throws an exception if the JSON is invalid * Decodes a JSON string into an associative array, throws an exception if the JSON is invalid
* *
@ -203,34 +204,19 @@ class Utilities
* @throws InvalidArgumentException If the input data is not valid Base64, * @throws InvalidArgumentException If the input data is not valid Base64,
* does not represent an image, or is not in the JPEG format. * does not represent an image, or is not in the JPEG format.
*/ */
public static function sanitizeBase64Jpeg(string $data): string public static function sanitizeJpeg(string $data): string
{ {
// Detect and strip the potential "data:image/...;base64," prefix, if present
if (str_contains($data, ','))
{
[, $data] = explode(',', $data, 2);
}
// Decode the Base64 string
$decodedData = base64_decode($data, true);
// Check if decoding succeeded
if ($decodedData === false)
{
throw new InvalidArgumentException("Invalid Base64 data.");
}
// Temporarily load the decoded data as an image // Temporarily load the decoded data as an image
$tempResource = imagecreatefromstring($decodedData); $tempResource = imagecreatefromstring($data);
// Validate that the decoded data is indeed an image // Validate that the decoded data is indeed an image
if ($tempResource === false) if ($tempResource === false)
{ {
throw new InvalidArgumentException("The Base64 data does not represent a valid image."); throw new InvalidArgumentException("The data does not represent a valid image.");
} }
// Validate MIME type using getimagesizefromstring // Validate MIME type using getimagesizefromstring
$imageInfo = getimagesizefromstring($decodedData); $imageInfo = getimagesizefromstring($data);
if ($imageInfo === false || $imageInfo['mime'] !== 'image/jpeg') if ($imageInfo === false || $imageInfo['mime'] !== 'image/jpeg')
{ {
imagedestroy($tempResource); // Cleanup resources imagedestroy($tempResource); // Cleanup resources
@ -252,6 +238,100 @@ class Utilities
return ob_get_clean(); return ob_get_clean();
} }
/**
* Resizes an image to a specified width and height while maintaining its aspect ratio.
* The resized image is centered on a black background matching the target dimensions.
*
* @param string $data The binary data of the source image.
* @param int $width The desired width of the resized image.
* @param int $height The desired height of the resized image.
* @return string The binary data of the resized image in PNG format.
* @throws InvalidArgumentException If the source image cannot be created from the provided data.
* @throws Exception If image processing fails during resizing.
*/
public static function resizeImage(string $data, int $width, int $height): string
{
try
{
// Create image resource from binary data
$sourceImage = imagecreatefromstring($data);
if (!$sourceImage)
{
throw new InvalidArgumentException("Failed to create image from provided data");
}
// Get original dimensions
$sourceWidth = imagesx($sourceImage);
$sourceHeight = imagesy($sourceImage);
// Calculate aspect ratios
$sourceRatio = $sourceWidth / $sourceHeight;
$targetRatio = $width / $height;
// Initialize dimensions for scaling
$scaleWidth = $width;
$scaleHeight = $height;
// Calculate scaling dimensions to maintain aspect ratio
if ($sourceRatio > $targetRatio)
{
// Source image is wider - scale by width
$scaleHeight = $width / $sourceRatio;
}
else
{
// Source image is taller - scale by height
$scaleWidth = $height * $sourceRatio;
}
// Create target image with desired dimensions
$targetImage = imagecreatetruecolor($width, $height);
if (!$targetImage)
{
throw new Exception("Failed to create target image");
}
// Fill background with black
$black = imagecolorallocate($targetImage, 0, 0, 0);
imagefill($targetImage, 0, 0, $black);
// Calculate padding to center the scaled image
$paddingX = ($width - $scaleWidth) / 2;
$paddingY = ($height - $scaleHeight) / 2;
// Enable alpha blending
imagealphablending($targetImage, true);
imagesavealpha($targetImage, true);
// Resize and copy the image with high-quality resampling
if (!imagecopyresampled($targetImage, $sourceImage, (int)$paddingX, (int)$paddingY, 0, 0, (int)$scaleWidth, (int)$scaleHeight, $sourceWidth, $sourceHeight))
{
throw new Exception("Failed to resize image");
}
// Start output buffering
ob_start();
// Output image as PNG (you can modify this to support other formats)
imagepng($targetImage);
// Return the image data
return ob_get_clean();
}
finally
{
if (isset($sourceImage))
{
imagedestroy($sourceImage);
}
if (isset($targetImage))
{
imagedestroy($targetImage);
}
}
}
/** /**
* Converts an array into a serialized string by joining the elements with a comma. * Converts an array into a serialized string by joining the elements with a comma.
* *
@ -284,4 +364,4 @@ class Utilities
{ {
return $responseCode >= 200 && $responseCode < 300; return $responseCode >= 200 && $responseCode < 300;
} }
} }

View file

@ -15,28 +15,31 @@ enum StandardError : int
case INTERNAL_SERVER_ERROR = -2000; case INTERNAL_SERVER_ERROR = -2000;
case SERVER_UNAVAILABLE = -2001; case SERVER_UNAVAILABLE = -2001;
// Client Errors
case BAD_REQUEST = -3000;
case METHOD_NOT_ALLOWED = -3001;
// Authentication/Cryptography Errors // Authentication/Cryptography Errors
case INVALID_PUBLIC_KEY = -3000; case INVALID_PUBLIC_KEY = -4000; // *
case SESSION_REQUIRED = -3001; case SESSION_REQUIRED = -5001; // *
case SESSION_NOT_FOUND = -3002; case SESSION_NOT_FOUND = -5002; // *
case SESSION_EXPIRED = -3003; case SESSION_EXPIRED = -5003; // *
case SESSION_DHE_REQUIRED = -3004; case SESSION_DHE_REQUIRED = -5004; // *
case ALREADY_AUTHENTICATED = -3005; case ALREADY_AUTHENTICATED = -6005;
case UNSUPPORTED_AUTHENTICATION_TYPE = -3006; case UNSUPPORTED_AUTHENTICATION_TYPE = -6006;
case AUTHENTICATION_REQUIRED = -3007; case AUTHENTICATION_REQUIRED = -6007;
case REGISTRATION_DISABLED = -3008; case REGISTRATION_DISABLED = -6008;
case CAPTCHA_NOT_AVAILABLE = -3009; case CAPTCHA_NOT_AVAILABLE = -6009;
case INCORRECT_CAPTCHA_ANSWER = -3010; case INCORRECT_CAPTCHA_ANSWER = -6010;
case CAPTCHA_EXPIRED = -3011; case CAPTCHA_EXPIRED = -6011;
// General Error Messages // General Error Messages
case PEER_NOT_FOUND = -4000; case PEER_NOT_FOUND = -7000;
case INVALID_USERNAME = -4001; case INVALID_USERNAME = -7001;
case USERNAME_ALREADY_EXISTS = -4002; case USERNAME_ALREADY_EXISTS = -7002;
case NOT_REGISTERED = -4003; case NOT_REGISTERED = -7003;
case METHOD_NOT_ALLOWED = -4004;
/** /**
* Returns the default generic message for the error * Returns the default generic message for the error

View file

@ -10,6 +10,7 @@
use Socialbox\Classes\StandardMethods\GetSessionState; use Socialbox\Classes\StandardMethods\GetSessionState;
use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\GetTermsOfService;
use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\Ping;
use Socialbox\Classes\StandardMethods\SettingsSetDisplayName;
use Socialbox\Classes\StandardMethods\SettingsSetPassword; use Socialbox\Classes\StandardMethods\SettingsSetPassword;
use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha;
use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha;
@ -83,6 +84,7 @@
self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest),
self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest),
self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest),
default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod()))
}; };

View file

@ -322,7 +322,8 @@
* Updates the display name of a registered peer based on the given unique identifier or RegisteredPeerRecord object. * Updates the display name of a registered peer based on the given unique identifier or RegisteredPeerRecord object.
* *
* @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord.
* @param string $name The new * @param string $name The new display name to set to the user
* @throws DatabaseOperationException Thrown if there was an error while trying to update the display name
*/ */
public static function updateDisplayName(string|RegisteredPeerRecord $peer, string $name): void public static function updateDisplayName(string|RegisteredPeerRecord $peer, string $name): void
{ {
@ -362,6 +363,60 @@
} }
} }
/**
* Updates the display picture of a registered peer in the database.
*
* @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord.
* @param string $displayPictureData The raw jpeg data of the display picture.
* @return void
* @throws DatabaseOperationException If there is an error during the database operation.
*/
public static function updateDisplayPicture(string|RegisteredPeerRecord $peer, string $displayPictureData): void
{
if(empty($uuid))
{
throw new InvalidArgumentException('The display picture UUID cannot be empty');
}
$uuid = Uuid::v4()->toRfc4122();
$displayPicturePath = Configuration::getStorageConfiguration()->getUserDisplayImagesPath() . DIRECTORY_SEPARATOR . $uuid . '.jpeg';
// Delete the file if it already exists
if(file_exists($displayPicturePath))
{
unlink($displayPicturePath);
}
// Write the file contents & set the permissions
file_put_contents($displayPicturePath, $displayPictureData);
chmod($displayPicturePath, 0644);
if(is_string($peer))
{
$peer = self::getPeer($peer);
}
if($peer->isExternal())
{
throw new InvalidArgumentException('Cannot update the display picture of an external peer');
}
Logger::getLogger()->verbose(sprintf("Updating display picture of peer %s to %s", $peer->getUuid(), $uuid));
try
{
$statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=? WHERE uuid=?');
$statement->bindParam(1, $uuid);
$peerUuid = $peer->getUuid();
$statement->bindParam(2, $peerUuid);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to update the display picture of the peer in the database', $e);
}
}
/** /**
* Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object. * Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object.
* *

View file

@ -14,6 +14,7 @@
private string $username; private string $username;
private string $server; private string $server;
private ?string $displayName; private ?string $displayName;
private ?string $displayPicture;
/** /**
* @var PeerFlags[] * @var PeerFlags[]
*/ */
@ -33,6 +34,7 @@
$this->username = $data['username']; $this->username = $data['username'];
$this->server = $data['server']; $this->server = $data['server'];
$this->displayName = $data['display_name'] ?? null; $this->displayName = $data['display_name'] ?? null;
$this->displayPicture = $data['display_picture'] ?? null;
if($data['flags']) if($data['flags'])
{ {
@ -105,16 +107,41 @@
return $this->displayName; return $this->displayName;
} }
/**
* Retrieves the display picture.
*
* @return string|null The display picture if set, or null otherwise.
*/
public function getDisplayPicture(): ?string
{
return $this->displayPicture;
}
/**
* Retrieves the flags.
*
* @return PeerFlags[] The flags.
*/
public function getFlags(): array public function getFlags(): array
{ {
return $this->flags; return $this->flags;
} }
/**
* Adds a flag to the current instance.
*
* @param PeerFlags $flag The flag to add.
*/
public function flagExists(PeerFlags $flag): bool public function flagExists(PeerFlags $flag): bool
{ {
return in_array($flag, $this->flags, true); return in_array($flag, $this->flags, true);
} }
/**
* Adds a flag to the current instance.
*
* @param PeerFlags $flag The flag to add.
*/
public function removeFlag(PeerFlags $flag): void public function removeFlag(PeerFlags $flag): void
{ {
$key = array_search($flag, $this->flags, true); $key = array_search($flag, $this->flags, true);
@ -182,6 +209,7 @@
'username' => $this->username, 'username' => $this->username,
'server' => $this->server, 'server' => $this->server,
'display_name' => $this->displayName, 'display_name' => $this->displayName,
'display_picture' => $this->displayPicture,
'flags' => PeerFlags::toString($this->flags), 'flags' => PeerFlags::toString($this->flags),
'enabled' => $this->enabled, 'enabled' => $this->enabled,
'created' => $this->created 'created' => $this->created