You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
9.2 KiB

  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Core\Controller;
  8. use OC\AppFramework\Utility\TimeFactory;
  9. use OCP\AppFramework\Controller;
  10. use OCP\AppFramework\Http;
  11. use OCP\AppFramework\Http\Attribute\FrontpageRoute;
  12. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  13. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  14. use OCP\AppFramework\Http\Attribute\OpenAPI;
  15. use OCP\AppFramework\Http\Attribute\PublicPage;
  16. use OCP\AppFramework\Http\FileDisplayResponse;
  17. use OCP\AppFramework\Http\JSONResponse;
  18. use OCP\AppFramework\Http\Response;
  19. use OCP\Files\File;
  20. use OCP\Files\IRootFolder;
  21. use OCP\Files\NotPermittedException;
  22. use OCP\IAvatarManager;
  23. use OCP\IL10N;
  24. use OCP\Image;
  25. use OCP\IRequest;
  26. use OCP\IUserManager;
  27. use Psr\Log\LoggerInterface;
  28. /**
  29. * Class AvatarController
  30. *
  31. * @package OC\Core\Controller
  32. */
  33. class AvatarController extends Controller {
  34. public function __construct(
  35. string $appName,
  36. IRequest $request,
  37. protected IAvatarManager $avatarManager,
  38. protected IL10N $l10n,
  39. protected IUserManager $userManager,
  40. protected IRootFolder $rootFolder,
  41. protected LoggerInterface $logger,
  42. protected ?string $userId,
  43. protected TimeFactory $timeFactory,
  44. protected GuestAvatarController $guestAvatarController,
  45. ) {
  46. parent::__construct($appName, $request);
  47. }
  48. /**
  49. * @NoSameSiteCookieRequired
  50. *
  51. * Get the dark avatar
  52. *
  53. * @param string $userId ID of the user
  54. * @param 64|512 $size Size of the avatar
  55. * @param bool $guestFallback Fallback to guest avatar if not found
  56. * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
  57. *
  58. * 200: Avatar returned
  59. * 201: Avatar returned
  60. * 404: Avatar not found
  61. */
  62. #[NoCSRFRequired]
  63. #[PublicPage]
  64. #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}/dark')]
  65. #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
  66. public function getAvatarDark(string $userId, int $size, bool $guestFallback = false) {
  67. if ($size <= 64) {
  68. if ($size !== 64) {
  69. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  70. }
  71. $size = 64;
  72. } else {
  73. if ($size !== 512) {
  74. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  75. }
  76. $size = 512;
  77. }
  78. try {
  79. $avatar = $this->avatarManager->getAvatar($userId);
  80. $avatarFile = $avatar->getFile($size, true);
  81. $response = new FileDisplayResponse(
  82. $avatarFile,
  83. Http::STATUS_OK,
  84. ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
  85. );
  86. } catch (\Exception $e) {
  87. if ($guestFallback) {
  88. return $this->guestAvatarController->getAvatarDark($userId, $size);
  89. }
  90. return new JSONResponse([], Http::STATUS_NOT_FOUND);
  91. }
  92. // Cache for 1 day
  93. $response->cacheFor(60 * 60 * 24, false, true);
  94. return $response;
  95. }
  96. /**
  97. * @NoSameSiteCookieRequired
  98. *
  99. * Get the avatar
  100. *
  101. * @param string $userId ID of the user
  102. * @param 64|512 $size Size of the avatar
  103. * @param bool $guestFallback Fallback to guest avatar if not found
  104. * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
  105. *
  106. * 200: Avatar returned
  107. * 201: Avatar returned
  108. * 404: Avatar not found
  109. */
  110. #[NoCSRFRequired]
  111. #[PublicPage]
  112. #[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}')]
  113. #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
  114. public function getAvatar(string $userId, int $size, bool $guestFallback = false) {
  115. if ($size <= 64) {
  116. if ($size !== 64) {
  117. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  118. }
  119. $size = 64;
  120. } else {
  121. if ($size !== 512) {
  122. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  123. }
  124. $size = 512;
  125. }
  126. try {
  127. $avatar = $this->avatarManager->getAvatar($userId);
  128. $avatarFile = $avatar->getFile($size);
  129. $response = new FileDisplayResponse(
  130. $avatarFile,
  131. Http::STATUS_OK,
  132. ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
  133. );
  134. } catch (\Exception $e) {
  135. if ($guestFallback) {
  136. return $this->guestAvatarController->getAvatar($userId, $size);
  137. }
  138. return new JSONResponse([], Http::STATUS_NOT_FOUND);
  139. }
  140. // Cache for 1 day
  141. $response->cacheFor(60 * 60 * 24, false, true);
  142. return $response;
  143. }
  144. #[NoAdminRequired]
  145. #[FrontpageRoute(verb: 'POST', url: '/avatar/')]
  146. public function postAvatar(?string $path = null): JSONResponse {
  147. $files = $this->request->getUploadedFile('files');
  148. if (isset($path)) {
  149. $path = stripslashes($path);
  150. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  151. /** @var File $node */
  152. $node = $userFolder->get($path);
  153. if (!($node instanceof File)) {
  154. return new JSONResponse(['data' => ['message' => $this->l10n->t('Please select a file.')]]);
  155. }
  156. if ($node->getSize() > 20 * 1024 * 1024) {
  157. return new JSONResponse(
  158. ['data' => ['message' => $this->l10n->t('File is too big')]],
  159. Http::STATUS_BAD_REQUEST
  160. );
  161. }
  162. if ($node->getMimeType() !== 'image/jpeg' && $node->getMimeType() !== 'image/png') {
  163. return new JSONResponse(
  164. ['data' => ['message' => $this->l10n->t('The selected file is not an image.')]],
  165. Http::STATUS_BAD_REQUEST
  166. );
  167. }
  168. try {
  169. $content = $node->getContent();
  170. } catch (NotPermittedException $e) {
  171. return new JSONResponse(
  172. ['data' => ['message' => $this->l10n->t('The selected file cannot be read.')]],
  173. Http::STATUS_BAD_REQUEST
  174. );
  175. }
  176. } elseif (!is_null($files)) {
  177. if (
  178. $files['error'][0] === 0
  179. && is_uploaded_file($files['tmp_name'][0])
  180. ) {
  181. if ($files['size'][0] > 20 * 1024 * 1024) {
  182. return new JSONResponse(
  183. ['data' => ['message' => $this->l10n->t('File is too big')]],
  184. Http::STATUS_BAD_REQUEST
  185. );
  186. }
  187. $content = file_get_contents($files['tmp_name'][0]);
  188. unlink($files['tmp_name'][0]);
  189. } else {
  190. $phpFileUploadErrors = [
  191. UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
  192. UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
  193. UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
  194. UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
  195. UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
  196. UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
  197. UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
  198. UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
  199. ];
  200. $message = $phpFileUploadErrors[$files['error'][0]] ?? $this->l10n->t('Invalid file provided');
  201. $this->logger->warning($message, ['app' => 'core']);
  202. return new JSONResponse(
  203. ['data' => ['message' => $message]],
  204. Http::STATUS_BAD_REQUEST
  205. );
  206. }
  207. } else {
  208. //Add imgfile
  209. return new JSONResponse(
  210. ['data' => ['message' => $this->l10n->t('No image or file provided')]],
  211. Http::STATUS_BAD_REQUEST
  212. );
  213. }
  214. try {
  215. $image = new Image();
  216. $image->loadFromData($content);
  217. $image->readExif($content);
  218. $image->fixOrientation();
  219. if ($image->valid()) {
  220. $mimeType = $image->mimeType();
  221. if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') {
  222. return new JSONResponse(
  223. ['data' => ['message' => $this->l10n->t('Unknown filetype')]],
  224. Http::STATUS_OK
  225. );
  226. }
  227. if ($image->width() === $image->height()) {
  228. try {
  229. $avatar = $this->avatarManager->getAvatar($this->userId);
  230. $avatar->set($image);
  231. return new JSONResponse(['status' => 'success']);
  232. } catch (\Throwable $e) {
  233. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  234. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
  235. }
  236. }
  237. return new JSONResponse(
  238. ['data' => 'notsquare', 'image' => 'data:' . $mimeType . ';base64,' . base64_encode($image->data())],
  239. Http::STATUS_OK
  240. );
  241. } else {
  242. return new JSONResponse(
  243. ['data' => ['message' => $this->l10n->t('Invalid image')]],
  244. Http::STATUS_OK
  245. );
  246. }
  247. } catch (\Exception $e) {
  248. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  249. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK);
  250. }
  251. }
  252. #[NoAdminRequired]
  253. #[FrontpageRoute(verb: 'DELETE', url: '/avatar/')]
  254. public function deleteAvatar(): JSONResponse {
  255. try {
  256. $avatar = $this->avatarManager->getAvatar($this->userId);
  257. $avatar->remove();
  258. return new JSONResponse();
  259. } catch (\Exception $e) {
  260. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  261. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
  262. }
  263. }
  264. }