UserAvatar.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Avatar;
  8. use OC\NotSquareException;
  9. use OC\User\User;
  10. use OCP\Files\NotFoundException;
  11. use OCP\Files\NotPermittedException;
  12. use OCP\Files\SimpleFS\ISimpleFile;
  13. use OCP\Files\SimpleFS\ISimpleFolder;
  14. use OCP\IConfig;
  15. use OCP\IImage;
  16. use OCP\IL10N;
  17. use Psr\Log\LoggerInterface;
  18. /**
  19. * This class represents a registered user's avatar.
  20. */
  21. class UserAvatar extends Avatar {
  22. public function __construct(
  23. private ISimpleFolder $folder,
  24. private IL10N $l,
  25. private User $user,
  26. LoggerInterface $logger,
  27. private IConfig $config,
  28. ) {
  29. parent::__construct($logger);
  30. }
  31. /**
  32. * Check if an avatar exists for the user
  33. */
  34. public function exists(): bool {
  35. return $this->folder->fileExists('avatar.jpg') || $this->folder->fileExists('avatar.png');
  36. }
  37. /**
  38. * Sets the users avatar.
  39. *
  40. * @param IImage|resource|string $data An image object, imagedata or path to set a new avatar
  41. * @throws \Exception if the provided file is not a jpg or png image
  42. * @throws \Exception if the provided image is not valid
  43. * @throws NotSquareException if the image is not square
  44. */
  45. public function set($data): void {
  46. $img = $this->getAvatarImage($data);
  47. $data = $img->data();
  48. $this->validateAvatar($img);
  49. $this->remove(true);
  50. $type = $this->getAvatarImageType($img);
  51. $file = $this->folder->newFile('avatar.' . $type);
  52. $file->putContent($data);
  53. try {
  54. $generated = $this->folder->getFile('generated');
  55. $generated->delete();
  56. } catch (NotFoundException $e) {
  57. //
  58. }
  59. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'false');
  60. $this->user->triggerChange('avatar', $file);
  61. }
  62. /**
  63. * Returns an image from several sources.
  64. *
  65. * @param IImage|resource|string|\GdImage $data An image object, imagedata or path to the avatar
  66. */
  67. private function getAvatarImage($data): IImage {
  68. if ($data instanceof IImage) {
  69. return $data;
  70. }
  71. $img = new \OCP\Image();
  72. if (
  73. (is_resource($data) && get_resource_type($data) === 'gd') ||
  74. (is_object($data) && get_class($data) === \GdImage::class)
  75. ) {
  76. $img->setResource($data);
  77. } elseif (is_resource($data)) {
  78. $img->loadFromFileHandle($data);
  79. } else {
  80. try {
  81. // detect if it is a path or maybe the images as string
  82. $result = @realpath($data);
  83. if ($result === false || $result === null) {
  84. $img->loadFromData($data);
  85. } else {
  86. $img->loadFromFile($data);
  87. }
  88. } catch (\Error $e) {
  89. $img->loadFromData($data);
  90. }
  91. }
  92. return $img;
  93. }
  94. /**
  95. * Returns the avatar image type.
  96. */
  97. private function getAvatarImageType(IImage $avatar): string {
  98. $type = substr($avatar->mimeType(), -3);
  99. if ($type === 'peg') {
  100. $type = 'jpg';
  101. }
  102. return $type;
  103. }
  104. /**
  105. * Validates an avatar image:
  106. * - must be "png" or "jpg"
  107. * - must be "valid"
  108. * - must be in square format
  109. *
  110. * @param IImage $avatar The avatar to validate
  111. * @throws \Exception if the provided file is not a jpg or png image
  112. * @throws \Exception if the provided image is not valid
  113. * @throws NotSquareException if the image is not square
  114. */
  115. private function validateAvatar(IImage $avatar): void {
  116. $type = $this->getAvatarImageType($avatar);
  117. if ($type !== 'jpg' && $type !== 'png') {
  118. throw new \Exception($this->l->t('Unknown filetype'));
  119. }
  120. if (!$avatar->valid()) {
  121. throw new \Exception($this->l->t('Invalid image'));
  122. }
  123. if (!($avatar->height() === $avatar->width())) {
  124. throw new NotSquareException($this->l->t('Avatar image is not square'));
  125. }
  126. }
  127. /**
  128. * Removes the users avatar.
  129. * @throws \OCP\Files\NotPermittedException
  130. * @throws \OCP\PreConditionNotMetException
  131. */
  132. public function remove(bool $silent = false): void {
  133. $avatars = $this->folder->getDirectoryListing();
  134. $this->config->setUserValue($this->user->getUID(), 'avatar', 'version',
  135. (string)((int)$this->config->getUserValue($this->user->getUID(), 'avatar', 'version', '0') + 1));
  136. foreach ($avatars as $avatar) {
  137. $avatar->delete();
  138. }
  139. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
  140. if (!$silent) {
  141. $this->user->triggerChange('avatar', '');
  142. }
  143. }
  144. /**
  145. * Get the extension of the avatar. If there is no avatar throw Exception
  146. *
  147. * @throws NotFoundException
  148. */
  149. private function getExtension(bool $generated, bool $darkTheme): string {
  150. if ($darkTheme && !$generated) {
  151. if ($this->folder->fileExists('avatar-dark.jpg')) {
  152. return 'jpg';
  153. } elseif ($this->folder->fileExists('avatar-dark.png')) {
  154. return 'png';
  155. }
  156. }
  157. if ($this->folder->fileExists('avatar.jpg')) {
  158. return 'jpg';
  159. } elseif ($this->folder->fileExists('avatar.png')) {
  160. return 'png';
  161. }
  162. throw new NotFoundException;
  163. }
  164. /**
  165. * Returns the avatar for an user.
  166. *
  167. * If there is no avatar file yet, one is generated.
  168. *
  169. * @throws NotFoundException
  170. * @throws \OCP\Files\NotPermittedException
  171. * @throws \OCP\PreConditionNotMetException
  172. */
  173. public function getFile(int $size, bool $darkTheme = false): ISimpleFile {
  174. $generated = $this->folder->fileExists('generated');
  175. try {
  176. $ext = $this->getExtension($generated, $darkTheme);
  177. } catch (NotFoundException $e) {
  178. if (!$data = $this->generateAvatarFromSvg(1024, $darkTheme)) {
  179. $data = $this->generateAvatar($this->getDisplayName(), 1024, $darkTheme);
  180. }
  181. $avatar = $this->folder->newFile($darkTheme ? 'avatar-dark.png' : 'avatar.png');
  182. $avatar->putContent($data);
  183. $ext = 'png';
  184. $this->folder->newFile('generated', '');
  185. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
  186. $generated = true;
  187. }
  188. if ($generated) {
  189. if ($size === -1) {
  190. $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $ext;
  191. } else {
  192. $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $size . '.' . $ext;
  193. }
  194. } else {
  195. if ($size === -1) {
  196. $path = 'avatar.' . $ext;
  197. } else {
  198. $path = 'avatar.' . $size . '.' . $ext;
  199. }
  200. }
  201. try {
  202. $file = $this->folder->getFile($path);
  203. } catch (NotFoundException $e) {
  204. if ($size <= 0) {
  205. throw new NotFoundException;
  206. }
  207. if ($generated) {
  208. if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) {
  209. $data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme);
  210. }
  211. } else {
  212. $avatar = new \OCP\Image();
  213. $file = $this->folder->getFile('avatar.' . $ext);
  214. $avatar->loadFromData($file->getContent());
  215. $avatar->resize($size);
  216. $data = $avatar->data();
  217. }
  218. try {
  219. $file = $this->folder->newFile($path);
  220. $file->putContent($data);
  221. } catch (NotPermittedException $e) {
  222. $this->logger->error('Failed to save avatar for ' . $this->user->getUID());
  223. throw new NotFoundException();
  224. }
  225. }
  226. if ($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) {
  227. $generated = $generated ? 'true' : 'false';
  228. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', $generated);
  229. }
  230. return $file;
  231. }
  232. /**
  233. * Returns the user display name.
  234. */
  235. public function getDisplayName(): string {
  236. return $this->user->getDisplayName();
  237. }
  238. /**
  239. * Handles user changes.
  240. *
  241. * @param string $feature The changed feature
  242. * @param mixed $oldValue The previous value
  243. * @param mixed $newValue The new value
  244. * @throws NotPermittedException
  245. * @throws \OCP\PreConditionNotMetException
  246. */
  247. public function userChanged(string $feature, $oldValue, $newValue): void {
  248. // If the avatar is not generated (so an uploaded image) we skip this
  249. if (!$this->folder->fileExists('generated')) {
  250. return;
  251. }
  252. $this->remove();
  253. }
  254. /**
  255. * Check if the avatar of a user is a custom uploaded one
  256. */
  257. public function isCustomAvatar(): bool {
  258. return $this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', 'false') !== 'true';
  259. }
  260. }