X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/06490f624c3923e945bf86a2930ff85c062a0bad..refs/pull/5725/head:/app/Uploads/UserAvatars.php diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index 9692b3f38..0cc640f22 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -5,6 +5,7 @@ namespace BookStack\Uploads; use BookStack\Exceptions\HttpFetchException; use BookStack\Http\HttpRequestService; use BookStack\Users\Models\User; +use BookStack\Util\WebSafeMimeSniffer; use Exception; use GuzzleHttp\Psr7\Request; use Illuminate\Support\Facades\Log; @@ -53,10 +54,37 @@ class UserAvatars } } + /** + * 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) + public function destroyAllForUser(User $user): void { $profileImages = Image::query()->where('type', '=', 'user') ->where('uploaded_to', '=', $user->id) @@ -70,7 +98,7 @@ class UserAvatars /** * Save an avatar image from an external service. * - * @throws Exception + * @throws HttpFetchException */ protected function saveAvatarImage(User $user, int $size = 500): Image { @@ -105,7 +133,7 @@ class UserAvatars } /** - * Gets an image from url and returns it as a string of image data. + * Get an image from a URL and return it as a string of image data. * * @throws HttpFetchException */ @@ -113,19 +141,33 @@ class UserAvatars { try { $client = $this->http->buildClient(5); - $response = $client->sendRequest(new Request('GET', $url)); - $imageData = (string) $response->getBody(); + $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(); @@ -135,7 +177,7 @@ class UserAvatars /** * Get the URL to fetch avatars from. */ - protected function getAvatarUrl(): string + public function getAvatarUrl(): string { $configOption = config('services.avatar_url'); if ($configOption === false) {