AvatarController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author John Molakvoæ <skjnldsv@protonmail.com>
  9. * @author Julien Veyssier <eneiluj@posteo.net>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  13. * @author Thomas Müller <thomas.mueller@tmit.eu>
  14. * @author Vincent Petry <vincent@nextcloud.com>
  15. * @author Kate Döen <kate.doeen@nextcloud.com>
  16. *
  17. * @license AGPL-3.0
  18. *
  19. * This code is free software: you can redistribute it and/or modify
  20. * it under the terms of the GNU Affero General Public License, version 3,
  21. * as published by the Free Software Foundation.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License, version 3,
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>
  30. *
  31. */
  32. namespace OC\Core\Controller;
  33. use OC\AppFramework\Utility\TimeFactory;
  34. use OCP\AppFramework\Controller;
  35. use OCP\AppFramework\Http;
  36. use OCP\AppFramework\Http\DataDisplayResponse;
  37. use OCP\AppFramework\Http\FileDisplayResponse;
  38. use OCP\AppFramework\Http\JSONResponse;
  39. use OCP\AppFramework\Http\Response;
  40. use OCP\Files\File;
  41. use OCP\Files\IRootFolder;
  42. use OCP\IAvatarManager;
  43. use OCP\ICache;
  44. use OCP\IL10N;
  45. use OCP\IRequest;
  46. use OCP\IUserManager;
  47. use Psr\Log\LoggerInterface;
  48. /**
  49. * Class AvatarController
  50. *
  51. * @package OC\Core\Controller
  52. */
  53. class AvatarController extends Controller {
  54. public function __construct(
  55. string $appName,
  56. IRequest $request,
  57. protected IAvatarManager $avatarManager,
  58. protected ICache $cache,
  59. protected IL10N $l10n,
  60. protected IUserManager $userManager,
  61. protected IRootFolder $rootFolder,
  62. protected LoggerInterface $logger,
  63. protected ?string $userId,
  64. protected TimeFactory $timeFactory,
  65. protected GuestAvatarController $guestAvatarController,
  66. ) {
  67. parent::__construct($appName, $request);
  68. }
  69. /**
  70. * @NoAdminRequired
  71. * @NoCSRFRequired
  72. * @NoSameSiteCookieRequired
  73. * @PublicPage
  74. *
  75. * Get the dark avatar
  76. *
  77. * @param string $userId ID of the user
  78. * @param int $size Size of the avatar
  79. * @param bool $guestFallback Fallback to guest avatar if not found
  80. * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
  81. *
  82. * 200: Avatar returned
  83. * 201: Avatar returned
  84. * 404: Avatar not found
  85. */
  86. public function getAvatarDark(string $userId, int $size, bool $guestFallback = false) {
  87. if ($size <= 64) {
  88. if ($size !== 64) {
  89. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  90. }
  91. $size = 64;
  92. } else {
  93. if ($size !== 512) {
  94. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  95. }
  96. $size = 512;
  97. }
  98. try {
  99. $avatar = $this->avatarManager->getAvatar($userId);
  100. $avatarFile = $avatar->getFile($size, true);
  101. $response = new FileDisplayResponse(
  102. $avatarFile,
  103. Http::STATUS_OK,
  104. ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
  105. );
  106. } catch (\Exception $e) {
  107. if ($guestFallback) {
  108. return $this->guestAvatarController->getAvatarDark($userId, (string)$size);
  109. }
  110. return new JSONResponse([], Http::STATUS_NOT_FOUND);
  111. }
  112. // Cache for 1 day
  113. $response->cacheFor(60 * 60 * 24, false, true);
  114. return $response;
  115. }
  116. /**
  117. * @NoAdminRequired
  118. * @NoCSRFRequired
  119. * @NoSameSiteCookieRequired
  120. * @PublicPage
  121. *
  122. * Get the avatar
  123. *
  124. * @param string $userId ID of the user
  125. * @param int $size Size of the avatar
  126. * @param bool $guestFallback Fallback to guest avatar if not found
  127. * @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
  128. *
  129. * 200: Avatar returned
  130. * 201: Avatar returned
  131. * 404: Avatar not found
  132. */
  133. public function getAvatar(string $userId, int $size, bool $guestFallback = false) {
  134. if ($size <= 64) {
  135. if ($size !== 64) {
  136. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  137. }
  138. $size = 64;
  139. } else {
  140. if ($size !== 512) {
  141. $this->logger->debug('Avatar requested in deprecated size ' . $size);
  142. }
  143. $size = 512;
  144. }
  145. try {
  146. $avatar = $this->avatarManager->getAvatar($userId);
  147. $avatarFile = $avatar->getFile($size);
  148. $response = new FileDisplayResponse(
  149. $avatarFile,
  150. Http::STATUS_OK,
  151. ['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
  152. );
  153. } catch (\Exception $e) {
  154. if ($guestFallback) {
  155. return $this->guestAvatarController->getAvatar($userId, (string)$size);
  156. }
  157. return new JSONResponse([], Http::STATUS_NOT_FOUND);
  158. }
  159. // Cache for 1 day
  160. $response->cacheFor(60 * 60 * 24, false, true);
  161. return $response;
  162. }
  163. /**
  164. * @NoAdminRequired
  165. */
  166. public function postAvatar(?string $path = null): JSONResponse {
  167. $files = $this->request->getUploadedFile('files');
  168. if (isset($path)) {
  169. $path = stripslashes($path);
  170. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  171. /** @var File $node */
  172. $node = $userFolder->get($path);
  173. if (!($node instanceof File)) {
  174. return new JSONResponse(['data' => ['message' => $this->l10n->t('Please select a file.')]]);
  175. }
  176. if ($node->getSize() > 20 * 1024 * 1024) {
  177. return new JSONResponse(
  178. ['data' => ['message' => $this->l10n->t('File is too big')]],
  179. Http::STATUS_BAD_REQUEST
  180. );
  181. }
  182. if ($node->getMimeType() !== 'image/jpeg' && $node->getMimeType() !== 'image/png') {
  183. return new JSONResponse(
  184. ['data' => ['message' => $this->l10n->t('The selected file is not an image.')]],
  185. Http::STATUS_BAD_REQUEST
  186. );
  187. }
  188. try {
  189. $content = $node->getContent();
  190. } catch (\OCP\Files\NotPermittedException $e) {
  191. return new JSONResponse(
  192. ['data' => ['message' => $this->l10n->t('The selected file cannot be read.')]],
  193. Http::STATUS_BAD_REQUEST
  194. );
  195. }
  196. } elseif (!is_null($files)) {
  197. if (
  198. $files['error'][0] === 0 &&
  199. is_uploaded_file($files['tmp_name'][0]) &&
  200. !\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0])
  201. ) {
  202. if ($files['size'][0] > 20 * 1024 * 1024) {
  203. return new JSONResponse(
  204. ['data' => ['message' => $this->l10n->t('File is too big')]],
  205. Http::STATUS_BAD_REQUEST
  206. );
  207. }
  208. $this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200);
  209. $content = $this->cache->get('avatar_upload');
  210. unlink($files['tmp_name'][0]);
  211. } else {
  212. $phpFileUploadErrors = [
  213. UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
  214. UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
  215. UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
  216. UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
  217. UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
  218. UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
  219. UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
  220. UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
  221. ];
  222. $message = $phpFileUploadErrors[$files['error'][0]] ?? $this->l10n->t('Invalid file provided');
  223. $this->logger->warning($message, ['app' => 'core']);
  224. return new JSONResponse(
  225. ['data' => ['message' => $message]],
  226. Http::STATUS_BAD_REQUEST
  227. );
  228. }
  229. } else {
  230. //Add imgfile
  231. return new JSONResponse(
  232. ['data' => ['message' => $this->l10n->t('No image or file provided')]],
  233. Http::STATUS_BAD_REQUEST
  234. );
  235. }
  236. try {
  237. $image = new \OCP\Image();
  238. $image->loadFromData($content);
  239. $image->readExif($content);
  240. $image->fixOrientation();
  241. if ($image->valid()) {
  242. $mimeType = $image->mimeType();
  243. if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') {
  244. return new JSONResponse(
  245. ['data' => ['message' => $this->l10n->t('Unknown filetype')]],
  246. Http::STATUS_OK
  247. );
  248. }
  249. if ($image->width() === $image->height()) {
  250. try {
  251. $avatar = $this->avatarManager->getAvatar($this->userId);
  252. $avatar->set($image);
  253. // Clean up
  254. $this->cache->remove('tmpAvatar');
  255. return new JSONResponse(['status' => 'success']);
  256. } catch (\Throwable $e) {
  257. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  258. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
  259. }
  260. }
  261. $this->cache->set('tmpAvatar', $image->data(), 7200);
  262. return new JSONResponse(
  263. ['data' => 'notsquare'],
  264. Http::STATUS_OK
  265. );
  266. } else {
  267. return new JSONResponse(
  268. ['data' => ['message' => $this->l10n->t('Invalid image')]],
  269. Http::STATUS_OK
  270. );
  271. }
  272. } catch (\Exception $e) {
  273. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  274. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK);
  275. }
  276. }
  277. /**
  278. * @NoAdminRequired
  279. */
  280. public function deleteAvatar(): JSONResponse {
  281. try {
  282. $avatar = $this->avatarManager->getAvatar($this->userId);
  283. $avatar->remove();
  284. return new JSONResponse();
  285. } catch (\Exception $e) {
  286. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  287. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
  288. }
  289. }
  290. /**
  291. * @NoAdminRequired
  292. *
  293. * @return JSONResponse|DataDisplayResponse
  294. */
  295. public function getTmpAvatar() {
  296. $tmpAvatar = $this->cache->get('tmpAvatar');
  297. if (is_null($tmpAvatar)) {
  298. return new JSONResponse(['data' => [
  299. 'message' => $this->l10n->t("No temporary profile picture available, try again")
  300. ]],
  301. Http::STATUS_NOT_FOUND);
  302. }
  303. $image = new \OCP\Image();
  304. $image->loadFromData($tmpAvatar);
  305. $resp = new DataDisplayResponse(
  306. $image->data() ?? '',
  307. Http::STATUS_OK,
  308. ['Content-Type' => $image->mimeType()]);
  309. $resp->setETag((string)crc32($image->data() ?? ''));
  310. $resp->cacheFor(0);
  311. $resp->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
  312. return $resp;
  313. }
  314. /**
  315. * @NoAdminRequired
  316. */
  317. public function postCroppedAvatar(?array $crop = null): JSONResponse {
  318. if (is_null($crop)) {
  319. return new JSONResponse(['data' => ['message' => $this->l10n->t("No crop data provided")]],
  320. Http::STATUS_BAD_REQUEST);
  321. }
  322. if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) {
  323. return new JSONResponse(['data' => ['message' => $this->l10n->t("No valid crop data provided")]],
  324. Http::STATUS_BAD_REQUEST);
  325. }
  326. $tmpAvatar = $this->cache->get('tmpAvatar');
  327. if (is_null($tmpAvatar)) {
  328. return new JSONResponse(['data' => [
  329. 'message' => $this->l10n->t("No temporary profile picture available, try again")
  330. ]],
  331. Http::STATUS_BAD_REQUEST);
  332. }
  333. $image = new \OCP\Image();
  334. $image->loadFromData($tmpAvatar);
  335. $image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h']));
  336. try {
  337. $avatar = $this->avatarManager->getAvatar($this->userId);
  338. $avatar->set($image);
  339. // Clean up
  340. $this->cache->remove('tmpAvatar');
  341. return new JSONResponse(['status' => 'success']);
  342. } catch (\OC\NotSquareException $e) {
  343. return new JSONResponse(['data' => ['message' => $this->l10n->t('Crop is not square')]],
  344. Http::STATUS_BAD_REQUEST);
  345. } catch (\Exception $e) {
  346. $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
  347. return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
  348. }
  349. }
  350. }