UserAvatar.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. $name = 'avatar-dark.';
  152. } else {
  153. $name = 'avatar.';
  154. }
  155. if ($this->folder->fileExists($name . 'jpg')) {
  156. return 'jpg';
  157. }
  158. if ($this->folder->fileExists($name . 'png')) {
  159. return 'png';
  160. }
  161. throw new NotFoundException;
  162. }
  163. /**
  164. * Returns the avatar for an user.
  165. *
  166. * If there is no avatar file yet, one is generated.
  167. *
  168. * @throws NotFoundException
  169. * @throws \OCP\Files\NotPermittedException
  170. * @throws \OCP\PreConditionNotMetException
  171. */
  172. public function getFile(int $size, bool $darkTheme = false): ISimpleFile {
  173. $generated = $this->folder->fileExists('generated');
  174. try {
  175. $ext = $this->getExtension($generated, $darkTheme);
  176. } catch (NotFoundException $e) {
  177. if (!$data = $this->generateAvatarFromSvg(1024, $darkTheme)) {
  178. $data = $this->generateAvatar($this->getDisplayName(), 1024, $darkTheme);
  179. }
  180. $avatar = $this->folder->newFile($darkTheme ? 'avatar-dark.png' : 'avatar.png');
  181. $avatar->putContent($data);
  182. $ext = 'png';
  183. $this->folder->newFile('generated', '');
  184. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
  185. $generated = true;
  186. }
  187. if ($generated) {
  188. if ($size === -1) {
  189. $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $ext;
  190. } else {
  191. $path = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $size . '.' . $ext;
  192. }
  193. } else {
  194. if ($size === -1) {
  195. $path = 'avatar.' . $ext;
  196. } else {
  197. $path = 'avatar.' . $size . '.' . $ext;
  198. }
  199. }
  200. try {
  201. $file = $this->folder->getFile($path);
  202. } catch (NotFoundException $e) {
  203. if ($size <= 0) {
  204. throw new NotFoundException;
  205. }
  206. if ($generated) {
  207. if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) {
  208. $data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme);
  209. }
  210. } else {
  211. $avatar = new \OCP\Image();
  212. $file = $this->folder->getFile('avatar.' . $ext);
  213. $avatar->loadFromData($file->getContent());
  214. $avatar->resize($size);
  215. $data = $avatar->data();
  216. }
  217. try {
  218. $file = $this->folder->newFile($path);
  219. $file->putContent($data);
  220. } catch (NotPermittedException $e) {
  221. $this->logger->error('Failed to save avatar for ' . $this->user->getUID());
  222. throw new NotFoundException();
  223. }
  224. }
  225. if ($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) {
  226. $generated = $generated ? 'true' : 'false';
  227. $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', $generated);
  228. }
  229. return $file;
  230. }
  231. /**
  232. * Returns the user display name.
  233. */
  234. public function getDisplayName(): string {
  235. return $this->user->getDisplayName();
  236. }
  237. /**
  238. * Handles user changes.
  239. *
  240. * @param string $feature The changed feature
  241. * @param mixed $oldValue The previous value
  242. * @param mixed $newValue The new value
  243. * @throws NotPermittedException
  244. * @throws \OCP\PreConditionNotMetException
  245. */
  246. public function userChanged(string $feature, $oldValue, $newValue): void {
  247. // If the avatar is not generated (so an uploaded image) we skip this
  248. if (!$this->folder->fileExists('generated')) {
  249. return;
  250. }
  251. $this->remove();
  252. }
  253. /**
  254. * Check if the avatar of a user is a custom uploaded one
  255. */
  256. public function isCustomAvatar(): bool {
  257. return $this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', 'false') !== 'true';
  258. }
  259. }