3 namespace BookStack\Uploads;
5 use BookStack\Exceptions\HttpFetchException;
6 use BookStack\Http\HttpRequestService;
7 use BookStack\Users\Models\User;
9 use GuzzleHttp\Psr7\Request;
10 use Illuminate\Support\Facades\Log;
11 use Illuminate\Support\Str;
12 use Psr\Http\Client\ClientExceptionInterface;
16 public function __construct(
17 protected ImageService $imageService,
18 protected HttpRequestService $http
23 * Fetch and assign an avatar image to the given user.
25 public function fetchAndAssignToUser(User $user): void
27 if (!$this->avatarFetchEnabled()) {
32 $this->destroyAllForUser($user);
33 $avatar = $this->saveAvatarImage($user);
34 $user->avatar()->associate($avatar);
36 } catch (Exception $e) {
37 Log::error('Failed to save user avatar image', ['exception' => $e]);
42 * Assign a new avatar image to the given user using the given image data.
44 public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void
47 $this->destroyAllForUser($user);
48 $avatar = $this->createAvatarImageFromData($user, $imageData, $extension);
49 $user->avatar()->associate($avatar);
51 } catch (Exception $e) {
52 Log::error('Failed to save user avatar image', ['exception' => $e]);
57 * Assign a new avatar image to the given user by fetching from a remote URL.
59 public function assignToUserFromUrl(User $user, string $avatarUrl, ?string $accessToken = null): void
61 // Quickly skip invalid or non-HTTP URLs
62 if (!$avatarUrl || !str_starts_with($avatarUrl, 'http')) {
67 $this->destroyAllForUser($user);
68 $imageData = $this->getAvatarImageData($avatarUrl, $accessToken);
69 $avatar = $this->createAvatarImageFromData($user, $imageData, 'png');
70 $user->avatar()->associate($avatar);
72 } catch (Exception $e) {
73 Log::error('Failed to save user avatar image from URL', [
76 'user_id' => $user->id,
82 * Destroy all user avatars uploaded to the given user.
84 public function destroyAllForUser(User $user): void
86 $profileImages = Image::query()->where('type', '=', 'user')
87 ->where('uploaded_to', '=', $user->id)
90 foreach ($profileImages as $image) {
91 $this->imageService->destroy($image);
96 * Save an avatar image from an external service.
98 * @throws HttpFetchException
100 protected function saveAvatarImage(User $user, int $size = 500): Image
102 $avatarUrl = $this->getAvatarUrl();
103 $email = strtolower(trim($user->email));
106 '${hash}' => md5($email),
108 '${email}' => urlencode($email),
111 $userAvatarUrl = strtr($avatarUrl, $replacements);
112 $imageData = $this->getAvatarImageData($userAvatarUrl);
114 return $this->createAvatarImageFromData($user, $imageData, 'png');
118 * Creates a new image instance and saves it in the system as a new user avatar image.
120 protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
122 $imageName = Str::random(10) . '-avatar.' . $extension;
124 $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
125 $image->created_by = $user->id;
126 $image->updated_by = $user->id;
133 * Gets an image from a URL (public or private) and returns it as a string of image data.
135 * @throws HttpFetchException
137 protected function getAvatarImageData(string $url, ?string $accessToken = null): string
141 if (!empty($accessToken)) {
142 $headers['Authorization'] = 'Bearer ' . $accessToken;
145 $client = $this->http->buildClient(5);
146 $response = $client->sendRequest(new Request('GET', $url, $headers));
148 if ($response->getStatusCode() !== 200) {
149 throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));
152 return (string) $response->getBody();
153 } catch (ClientExceptionInterface $exception) {
154 throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);
159 * Check if fetching external avatars is enabled.
161 public function avatarFetchEnabled(): bool
163 $fetchUrl = $this->getAvatarUrl();
165 return str_starts_with($fetchUrl, 'http');
169 * Get the URL to fetch avatars from.
171 public function getAvatarUrl(): string
173 $configOption = config('services.avatar_url');
174 if ($configOption === false) {
178 $url = trim($configOption);
180 if (empty($url) && !config('services.disable_services')) {
181 $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';