| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace BookStack\Uploads; | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | use BookStack\Exceptions\HttpFetchException; | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  | use BookStack\Http\HttpRequestService; | 
					
						
							| 
									
										
										
										
											2023-05-18 00:56:55 +08:00
										 |  |  | use BookStack\Users\Models\User; | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  | use BookStack\Util\WebSafeMimeSniffer; | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  | use Exception; | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  | use GuzzleHttp\Psr7\Request; | 
					
						
							| 
									
										
										
										
											2021-01-10 21:29:13 +08:00
										 |  |  | use Illuminate\Support\Facades\Log; | 
					
						
							| 
									
										
										
										
											2022-07-26 19:10:19 +08:00
										 |  |  | use Illuminate\Support\Str; | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  | use Psr\Http\Client\ClientExceptionInterface; | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | class UserAvatars | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  |     public function __construct( | 
					
						
							|  |  |  |         protected ImageService $imageService, | 
					
						
							|  |  |  |         protected HttpRequestService $http | 
					
						
							|  |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Fetch and assign an avatar image to the given user. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function fetchAndAssignToUser(User $user): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         if (!$this->avatarFetchEnabled()) { | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |             $this->destroyAllForUser($user); | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |             $avatar = $this->saveAvatarImage($user); | 
					
						
							|  |  |  |             $user->avatar()->associate($avatar); | 
					
						
							|  |  |  |             $user->save(); | 
					
						
							| 
									
										
										
										
											2023-06-19 14:47:47 +08:00
										 |  |  |         } catch (Exception $e) { | 
					
						
							| 
									
										
										
										
											2023-06-14 20:09:52 +08:00
										 |  |  |             Log::error('Failed to save user avatar image', ['exception' => $e]); | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * 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(); | 
					
						
							| 
									
										
										
										
											2023-06-19 14:47:47 +08:00
										 |  |  |         } catch (Exception $e) { | 
					
						
							| 
									
										
										
										
											2023-06-14 20:09:52 +08:00
										 |  |  |             Log::error('Failed to save user avatar image', ['exception' => $e]); | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-21 00:21:46 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Assign a new avatar image to the given user by fetching from a remote URL. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  |     public function assignToUserFromUrl(User $user, string $avatarUrl): void | 
					
						
							| 
									
										
										
										
											2025-01-21 00:21:46 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             $this->destroyAllForUser($user); | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  |             $imageData = $this->getAvatarImageData($avatarUrl); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             $mime = (new WebSafeMimeSniffer())->sniff($imageData); | 
					
						
							|  |  |  |             [$format, $type] = explode('/', $mime, 2); | 
					
						
							| 
									
										
										
										
											2025-05-24 21:36:36 +08:00
										 |  |  |             if ($format !== 'image' || !ImageService::isExtensionSupported($type)) { | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  |                 return; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             $avatar = $this->createAvatarImageFromData($user, $imageData, $type); | 
					
						
							| 
									
										
										
										
											2025-01-21 00:21:46 +08:00
										 |  |  |             $user->avatar()->associate($avatar); | 
					
						
							|  |  |  |             $user->save(); | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             Log::error('Failed to save user avatar image from URL', [ | 
					
						
							| 
									
										
										
										
											2025-05-25 00:56:21 +08:00
										 |  |  |                 'exception' => $e->getMessage(), | 
					
						
							| 
									
										
										
										
											2025-01-21 00:21:46 +08:00
										 |  |  |                 'url'       => $avatarUrl, | 
					
						
							|  |  |  |                 'user_id'   => $user->id, | 
					
						
							|  |  |  |             ]); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Destroy all user avatars uploaded to the given user. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-09-19 02:04:59 +08:00
										 |  |  |     public function destroyAllForUser(User $user): void | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $profileImages = Image::query()->where('type', '=', 'user') | 
					
						
							|  |  |  |             ->where('uploaded_to', '=', $user->id) | 
					
						
							|  |  |  |             ->get(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($profileImages as $image) { | 
					
						
							|  |  |  |             $this->imageService->destroy($image); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Save an avatar image from an external service. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2023-09-19 02:04:59 +08:00
										 |  |  |      * @throws HttpFetchException | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function saveAvatarImage(User $user, int $size = 500): Image | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $avatarUrl = $this->getAvatarUrl(); | 
					
						
							|  |  |  |         $email = strtolower(trim($user->email)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $replacements = [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             '${hash}'  => md5($email), | 
					
						
							|  |  |  |             '${size}'  => $size, | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |             '${email}' => urlencode($email), | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $userAvatarUrl = strtr($avatarUrl, $replacements); | 
					
						
							|  |  |  |         $imageData = $this->getAvatarImageData($userAvatarUrl); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-25 01:45:08 +08:00
										 |  |  |         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 | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2022-07-26 19:10:19 +08:00
										 |  |  |         $imageName = Str::random(10) . '-avatar.' . $extension; | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id); | 
					
						
							|  |  |  |         $image->created_by = $user->id; | 
					
						
							|  |  |  |         $image->updated_by = $user->id; | 
					
						
							|  |  |  |         $image->save(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $image; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  |      * Get an image from a URL and return it as a string of image data. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2023-06-14 20:09:52 +08:00
										 |  |  |      * @throws HttpFetchException | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2025-05-24 21:02:37 +08:00
										 |  |  |     protected function getAvatarImageData(string $url): string | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  |             $client = $this->http->buildClient(5); | 
					
						
							| 
									
										
										
										
											2025-05-25 00:56:21 +08:00
										 |  |  |             $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}"); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-01-21 00:21:46 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-19 02:04:59 +08:00
										 |  |  |             if ($response->getStatusCode() !== 200) { | 
					
						
							|  |  |  |                 throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url])); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return (string) $response->getBody(); | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  |         } catch (ClientExceptionInterface $exception) { | 
					
						
							| 
									
										
										
										
											2023-06-14 20:09:52 +08:00
										 |  |  |             throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception); | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Check if fetching external avatars is enabled. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-09-19 22:53:01 +08:00
										 |  |  |     public function avatarFetchEnabled(): bool | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $fetchUrl = $this->getAvatarUrl(); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-09 00:16:57 +08:00
										 |  |  |         return str_starts_with($fetchUrl, 'http'); | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the URL to fetch avatars from. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-09-19 22:53:01 +08:00
										 |  |  |     public function getAvatarUrl(): string | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2022-07-26 19:10:19 +08:00
										 |  |  |         $configOption = config('services.avatar_url'); | 
					
						
							|  |  |  |         if ($configOption === false) { | 
					
						
							|  |  |  |             return ''; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $url = trim($configOption); | 
					
						
							| 
									
										
										
										
											2020-12-09 07:46:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (empty($url) && !config('services.disable_services')) { | 
					
						
							|  |  |  |             $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon'; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $url; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-08 06:24:05 +08:00
										 |  |  | } |