]> BookStack Code Mirror - bookstack/blob - app/Uploads/UserAvatars.php
fix: Actually check if we have correct data
[bookstack] / app / Uploads / UserAvatars.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use BookStack\Exceptions\HttpFetchException;
6 use BookStack\Http\HttpRequestService;
7 use BookStack\Users\Models\User;
8 use Exception;
9 use GuzzleHttp\Psr7\Request;
10 use Illuminate\Support\Facades\Log;
11 use Illuminate\Support\Str;
12 use Psr\Http\Client\ClientExceptionInterface;
13
14 class UserAvatars
15 {
16     public function __construct(
17         protected ImageService $imageService,
18         protected HttpRequestService $http
19     ) {
20     }
21
22     /**
23      * Fetch and assign an avatar image to the given user.
24      */
25     public function fetchAndAssignToUser(User $user): void
26     {
27         if (!$this->avatarFetchEnabled()) {
28             return;
29         }
30
31         try {
32             $this->destroyAllForUser($user);
33             $avatar = $this->saveAvatarImage($user);
34             $user->avatar()->associate($avatar);
35             $user->save();
36         } catch (Exception $e) {
37             Log::error('Failed to save user avatar image', ['exception' => $e]);
38         }
39     }
40
41     /**
42      * Assign a new avatar image to the given user using the given image data.
43      */
44     public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void
45     {
46         try {
47             $this->destroyAllForUser($user);
48             $avatar = $this->createAvatarImageFromData($user, $imageData, $extension);
49             $user->avatar()->associate($avatar);
50             $user->save();
51         } catch (Exception $e) {
52             Log::error('Failed to save user avatar image', ['exception' => $e]);
53         }
54     }
55
56     /**
57      * Destroy all user avatars uploaded to the given user.
58      */
59     public function destroyAllForUser(User $user): void
60     {
61         $profileImages = Image::query()->where('type', '=', 'user')
62             ->where('uploaded_to', '=', $user->id)
63             ->get();
64
65         foreach ($profileImages as $image) {
66             $this->imageService->destroy($image);
67         }
68     }
69
70     /**
71      * Save an avatar image from an external service.
72      *
73      * @throws HttpFetchException
74      */
75     protected function saveAvatarImage(User $user, int $size = 500): Image
76     {
77         $avatarUrl = $this->getAvatarUrl();
78         $email = strtolower(trim($user->email));
79
80         $replacements = [
81             '${hash}'  => md5($email),
82             '${size}'  => $size,
83             '${email}' => urlencode($email),
84         ];
85
86         $userAvatarUrl = strtr($avatarUrl, $replacements);
87         $imageData = $this->getAvatarImageData($userAvatarUrl);
88
89         return $this->createAvatarImageFromData($user, $imageData, 'png');
90     }
91
92     /**
93      * Creates a new image instance and saves it in the system as a new user avatar image.
94      */
95     protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
96     {
97         $imageName = Str::random(10) . '-avatar.' . $extension;
98
99         $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
100         $image->created_by = $user->id;
101         $image->updated_by = $user->id;
102         $image->save();
103
104         return $image;
105     }
106
107     /**
108      * Gets an image from url and returns it as a string of image data.
109      *
110      * @throws HttpFetchException
111      */
112     protected function getAvatarImageData(string $url): string
113     {
114         try {
115             $client = $this->http->buildClient(5);
116             $response = $client->sendRequest(new Request('GET', $url));
117             if ($response->getStatusCode() !== 200) {
118                 throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));
119             }
120
121             return (string) $response->getBody();
122         } catch (ClientExceptionInterface $exception) {
123             throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);
124         }
125     }
126
127     /**
128      * Check if fetching external avatars is enabled.
129      */
130     protected function avatarFetchEnabled(): bool
131     {
132         $fetchUrl = $this->getAvatarUrl();
133
134         return str_starts_with($fetchUrl, 'http');
135     }
136
137     /**
138      * Get the URL to fetch avatars from.
139      */
140     protected function getAvatarUrl(): string
141     {
142         $configOption = config('services.avatar_url');
143         if ($configOption === false) {
144             return '';
145         }
146
147         $url = trim($configOption);
148
149         if (empty($url) && !config('services.disable_services')) {
150             $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
151         }
152
153         return $url;
154     }
155 }