X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/a6633642232efd164d4708967ab59e498fbff896..refs/pull/5685/head:/app/Uploads/UserAvatars.php diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index b3b9d5951..0cc640f22 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -1,19 +1,23 @@ -imageService = $imageService; - $this->http = $http; + public function __construct( + protected ImageService $imageService, + protected HttpRequestService $http + ) { } /** @@ -26,17 +30,75 @@ class UserAvatars } try { + $this->destroyAllForUser($user); $avatar = $this->saveAvatarImage($user); $user->avatar()->associate($avatar); $user->save(); } catch (Exception $e) { - Log::error('Failed to save user avatar image'); + Log::error('Failed to save user avatar image', ['exception' => $e]); + } + } + + /** + * Assign a new avatar image to the given user using the given image data. + */ + public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void + { + try { + $this->destroyAllForUser($user); + $avatar = $this->createAvatarImageFromData($user, $imageData, $extension); + $user->avatar()->associate($avatar); + $user->save(); + } catch (Exception $e) { + Log::error('Failed to save user avatar image', ['exception' => $e]); + } + } + + /** + * Assign a new avatar image to the given user by fetching from a remote URL. + */ + public function assignToUserFromUrl(User $user, string $avatarUrl): void + { + try { + $this->destroyAllForUser($user); + $imageData = $this->getAvatarImageData($avatarUrl); + + $mime = (new WebSafeMimeSniffer())->sniff($imageData); + [$format, $type] = explode('/', $mime, 2); + if ($format !== 'image' || !ImageService::isExtensionSupported($type)) { + return; + } + + $avatar = $this->createAvatarImageFromData($user, $imageData, $type); + $user->avatar()->associate($avatar); + $user->save(); + } catch (Exception $e) { + Log::error('Failed to save user avatar image from URL', [ + 'exception' => $e->getMessage(), + 'url' => $avatarUrl, + 'user_id' => $user->id, + ]); + } + } + + /** + * Destroy all user avatars uploaded to the given user. + */ + public function destroyAllForUser(User $user): void + { + $profileImages = Image::query()->where('type', '=', 'user') + ->where('uploaded_to', '=', $user->id) + ->get(); + + foreach ($profileImages as $image) { + $this->imageService->destroy($image); } } /** * Save an avatar image from an external service. - * @throws Exception + * + * @throws HttpFetchException */ protected function saveAvatarImage(User $user, int $size = 500): Image { @@ -44,15 +106,24 @@ class UserAvatars $email = strtolower(trim($user->email)); $replacements = [ - '${hash}' => md5($email), - '${size}' => $size, + '${hash}' => md5($email), + '${size}' => $size, '${email}' => urlencode($email), ]; $userAvatarUrl = strtr($avatarUrl, $replacements); - $imageName = str_replace(' ', '-', $user->id . '-avatar.png'); $imageData = $this->getAvatarImageData($userAvatarUrl); + return $this->createAvatarImageFromData($user, $imageData, 'png'); + } + + /** + * Creates a new image instance and saves it in the system as a new user avatar image. + */ + protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image + { + $imageName = Str::random(10) . '-avatar.' . $extension; + $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id); $image->created_by = $user->id; $image->updated_by = $user->id; @@ -62,34 +133,58 @@ class UserAvatars } /** - * Gets an image from url and returns it as a string of image data. - * @throws Exception + * Get an image from a URL and return it as a string of image data. + * + * @throws HttpFetchException */ protected function getAvatarImageData(string $url): string { try { - $imageData = $this->http->fetch($url); - } catch (HttpFetchException $exception) { - throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); + $client = $this->http->buildClient(5); + $responseCount = 0; + + do { + $response = $client->sendRequest(new Request('GET', $url)); + $responseCount++; + $isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302); + $url = $response->getHeader('Location')[0] ?? ''; + } while ($responseCount < 3 && $isRedirect && is_string($url) && str_starts_with($url, 'http')); + + if ($responseCount === 3) { + throw new HttpFetchException("Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}"); + } + + if ($response->getStatusCode() !== 200) { + throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url])); + } + + return (string) $response->getBody(); + } catch (ClientExceptionInterface $exception) { + throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception); } - return $imageData; } /** * Check if fetching external avatars is enabled. */ - protected function avatarFetchEnabled(): bool + public function avatarFetchEnabled(): bool { $fetchUrl = $this->getAvatarUrl(); - return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0; + + return str_starts_with($fetchUrl, 'http'); } /** * Get the URL to fetch avatars from. */ - protected function getAvatarUrl(): string + public function getAvatarUrl(): string { - $url = trim(config('services.avatar_url')); + $configOption = config('services.avatar_url'); + if ($configOption === false) { + return ''; + } + + $url = trim($configOption); if (empty($url) && !config('services.disable_services')) { $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon'; @@ -97,5 +192,4 @@ class UserAvatars return $url; } - -} \ No newline at end of file +}