avatarsettings.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Upload an avatar
  18. *
  19. * @category Settings
  20. * @package GNUsocial
  21. *
  22. * @author Evan Prodromou <evan@status.net>
  23. * @author Zach Copley <zach@status.net>
  24. * @author Diogo Cordeiro <diogo@fc.up.pt>
  25. * @copyright 2008-2009, 2020 Free Software Foundation http://fsf.org
  26. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  27. */
  28. defined('GNUSOCIAL') || die;
  29. /**
  30. * Upload an avatar
  31. *
  32. * We use jCrop plugin for jQuery to crop the image after upload.
  33. *
  34. * @category Settings
  35. * @package StatusNet
  36. *
  37. * @author Evan Prodromou <evan@status.net>
  38. * @author Zach Copley <zach@status.net>
  39. * @author Sarven Capadisli <csarven@status.net>
  40. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  41. *
  42. * @see http://status.net/
  43. */
  44. class AvatarsettingsAction extends SettingsAction
  45. {
  46. public $filedata;
  47. public $mode;
  48. public $imagefile;
  49. public $filename;
  50. public function prepare(array $args = [])
  51. {
  52. $avatarpath = Avatar::path('');
  53. if (!is_writable($avatarpath)) {
  54. throw new Exception(_m("The administrator of your site needs to
  55. add write permissions on the avatar upload folder before
  56. you're able to set one."));
  57. }
  58. parent::prepare($args);
  59. return true;
  60. }
  61. /**
  62. * Title of the page
  63. *
  64. * @return string Title of the page
  65. * @throws Exception
  66. *
  67. */
  68. public function title()
  69. {
  70. // TRANS: Title for avatar upload page.
  71. return _m('Avatar');
  72. }
  73. /**
  74. * Instructions for use
  75. *
  76. * @return string instructions for use
  77. * @throws Exception
  78. *
  79. */
  80. public function getInstructions()
  81. {
  82. // TRANS: Instruction for avatar upload page.
  83. // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
  84. return sprintf(
  85. _m('You can upload your personal avatar. The maximum file size is %s.'),
  86. ImageFile::maxFileSize()
  87. );
  88. }
  89. /**
  90. * Content area of the page
  91. *
  92. * Shows a form for uploading an avatar. Currently overrides FormAction's showContent
  93. * since we haven't made classes out of AvatarCropForm and AvatarUploadForm.
  94. *
  95. * @return void
  96. */
  97. public function showContent()
  98. {
  99. if ($this->mode == 'crop') {
  100. $this->showCropForm();
  101. } else {
  102. $this->showUploadForm();
  103. }
  104. }
  105. public function showUploadForm()
  106. {
  107. // If we detect that the user didn't finish setting a new avatar, remove the temporary file.
  108. if (isset($_SESSION['FILEDATA'])) {
  109. @unlink($_SESSION['FILEDATA']['filepath']);
  110. unset($_SESSION['FILEDATA']);
  111. }
  112. $this->elementStart('form', ['enctype' => 'multipart/form-data',
  113. 'method' => 'post',
  114. 'id' => 'form_settings_avatar',
  115. 'class' => 'form_settings',
  116. 'action' => common_local_url('avatarsettings'),]);
  117. $this->elementStart('fieldset');
  118. // TRANS: Avatar upload page form legend.
  119. $this->element('legend', null, _m('Avatar settings'));
  120. $this->hidden('token', common_session_token());
  121. if (Event::handle('StartAvatarFormData', [$this])) {
  122. $this->elementStart('ul', 'form_data');
  123. try {
  124. $original = Avatar::getUploaded($this->scoped);
  125. $this->elementStart('li', ['id' => 'avatar_original',
  126. 'class' => 'avatar_view',]);
  127. // TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2).
  128. $this->element('h2', null, _m('Original'));
  129. $this->elementStart('div', ['id' => 'avatar_original_view']);
  130. $this->element('img', ['src' => $original->displayUrl(),
  131. 'width' => $original->width,
  132. 'height' => $original->height,
  133. 'alt' => $this->scoped->getNickname(),]);
  134. $this->elementEnd('div');
  135. $this->elementEnd('li');
  136. } catch (NoAvatarException $e) {
  137. // No original avatar found!
  138. }
  139. try {
  140. $avatar = $this->scoped->getAvatar(AVATAR_PROFILE_SIZE);
  141. $this->elementStart('li', ['id' => 'avatar_preview',
  142. 'class' => 'avatar_view',]);
  143. // TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2).
  144. $this->element('h2', null, _m('Preview'));
  145. $this->elementStart('div', ['id' => 'avatar_preview_view']);
  146. $this->element('img', ['src' => $avatar->displayUrl(),
  147. 'width' => AVATAR_PROFILE_SIZE,
  148. 'height' => AVATAR_PROFILE_SIZE,
  149. 'alt' => $this->scoped->getNickname(),]);
  150. $this->elementEnd('div');
  151. if (!empty($avatar->filename)) {
  152. // TRANS: Button on avatar upload page to delete current avatar.
  153. $this->submit('delete', _m('BUTTON', 'Delete'));
  154. }
  155. $this->elementEnd('li');
  156. } catch (NoAvatarException $e) {
  157. // No previously uploaded avatar to preview.
  158. }
  159. $this->elementStart('li', ['id' => 'settings_attach']);
  160. $this->element('input', ['name' => 'MAX_FILE_SIZE',
  161. 'type' => 'hidden',
  162. 'id' => 'MAX_FILE_SIZE',
  163. 'value' => ImageFile::maxFileSizeInt(),]);
  164. $this->element('input', ['name' => 'avatarfile',
  165. 'type' => 'file',
  166. 'id' => 'avatarfile',]);
  167. $this->elementEnd('li');
  168. $this->elementEnd('ul');
  169. $this->elementStart('ul', 'form_actions');
  170. $this->elementStart('li');
  171. // TRANS: Button on avatar upload page to upload an avatar.
  172. $this->submit('upload', _m('BUTTON', 'Upload'));
  173. $this->elementEnd('li');
  174. $this->elementEnd('ul');
  175. }
  176. Event::handle('EndAvatarFormData', [$this]);
  177. $this->elementEnd('fieldset');
  178. $this->elementEnd('form');
  179. }
  180. public function showCropForm()
  181. {
  182. $this->elementStart('form', ['method' => 'post',
  183. 'id' => 'form_settings_avatar',
  184. 'class' => 'form_settings',
  185. 'action' => common_local_url('avatarsettings'),]);
  186. $this->elementStart('fieldset');
  187. // TRANS: Avatar upload page crop form legend.
  188. $this->element('legend', null, _m('Avatar settings'));
  189. $this->hidden('token', common_session_token());
  190. $this->elementStart('ul', 'form_data');
  191. $this->elementStart(
  192. 'li',
  193. ['id' => 'avatar_original',
  194. 'class' => 'avatar_view',]
  195. );
  196. // TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2).
  197. $this->element('h2', null, _m('Original'));
  198. $this->elementStart('div', ['id' => 'avatar_original_view']);
  199. $this->element('img', ['src' => Avatar::url($this->filedata['filename']),
  200. 'width' => $this->filedata['width'],
  201. 'height' => $this->filedata['height'],
  202. 'alt' => $this->scoped->getNickname(),]);
  203. $this->elementEnd('div');
  204. $this->elementEnd('li');
  205. $this->elementStart(
  206. 'li',
  207. ['id' => 'avatar_preview',
  208. 'class' => 'avatar_view',]
  209. );
  210. // TRANS: Header on avatar upload crop form for thumbnail of to be used rendition of uploaded avatar (h2).
  211. $this->element('h2', null, _m('Preview'));
  212. $this->elementStart('div', ['id' => 'avatar_preview_view']);
  213. $this->element('img', ['src' => Avatar::url($this->filedata['filename']),
  214. 'width' => AVATAR_PROFILE_SIZE,
  215. 'height' => AVATAR_PROFILE_SIZE,
  216. 'alt' => $this->scoped->getNickname(),]);
  217. $this->elementEnd('div');
  218. foreach (['avatar_crop_x', 'avatar_crop_y',
  219. 'avatar_crop_w', 'avatar_crop_h',] as $crop_info) {
  220. $this->element('input', ['name' => $crop_info,
  221. 'type' => 'hidden',
  222. 'id' => $crop_info,]);
  223. }
  224. // TRANS: Button on avatar upload crop form to confirm a selected crop as avatar.
  225. $this->submit('crop', _m('BUTTON', 'Crop'));
  226. $this->elementEnd('li');
  227. $this->elementEnd('ul');
  228. $this->elementEnd('fieldset');
  229. $this->elementEnd('form');
  230. }
  231. /**
  232. * @return string
  233. * @throws NoResultException
  234. * @throws NoUploadedMediaException
  235. * @throws ServerException
  236. * @throws UnsupportedMediaException
  237. * @throws UseFileAsThumbnailException
  238. * @throws Exception
  239. *
  240. * @throws ClientException
  241. */
  242. protected function doPost()
  243. {
  244. if (Event::handle('StartAvatarSaveForm', [$this])) {
  245. if ($this->trimmed('upload')) {
  246. return $this->uploadAvatar();
  247. } elseif ($this->trimmed('crop')) {
  248. return $this->cropAvatar();
  249. } elseif ($this->trimmed('delete')) {
  250. return $this->deleteAvatar();
  251. } else {
  252. // TRANS: Unexpected validation error on avatar upload form.
  253. throw new ClientException(_m('Unexpected form submission.'));
  254. }
  255. Event::handle('EndAvatarSaveForm', [$this]);
  256. }
  257. }
  258. /**
  259. * Handle an image upload
  260. *
  261. * Does all the magic for handling an image upload, and crops the
  262. * image by default.
  263. *
  264. * @return string
  265. * @throws NoResultException
  266. * @throws NoUploadedMediaException
  267. * @throws ServerException
  268. * @throws UnsupportedMediaException
  269. * @throws UseFileAsThumbnailException
  270. *
  271. * @throws ClientException
  272. */
  273. public function uploadAvatar(): string
  274. {
  275. // ImageFile throws exception if something goes wrong, which we'll
  276. // pick up and show as an error message above the form.
  277. $imagefile = ImageFile::fromUpload('avatarfile');
  278. $type = $imagefile->preferredType();
  279. $filename = Avatar::filename(
  280. $this->scoped->getID(),
  281. image_type_to_extension($type),
  282. null,
  283. 'tmp' . common_timestamp()
  284. );
  285. $filepath = Avatar::path($filename);
  286. $imagefile = $imagefile->copyTo($filepath);
  287. $filedata = [
  288. 'filename' => $filename,
  289. 'filepath' => $filepath,
  290. 'width' => $imagefile->width,
  291. 'height' => $imagefile->height,
  292. 'type' => $type,
  293. ];
  294. $_SESSION['FILEDATA'] = $filedata;
  295. $this->filedata = $filedata;
  296. $this->mode = 'crop';
  297. // TRANS: Avatar upload form instruction after uploading a file.
  298. return _m('Pick a square area of the image to be your avatar.');
  299. }
  300. /**
  301. * Handle the results of jcrop.
  302. *
  303. * @return string
  304. * @throws NoResultException
  305. * @throws ServerException
  306. * @throws UnsupportedMediaException
  307. *
  308. * @throws ClientException
  309. */
  310. public function cropAvatar()
  311. {
  312. $filedata = $_SESSION['FILEDATA'];
  313. if (empty($filedata)) {
  314. // TRANS: Server error displayed if an avatar upload went wrong somehow server side.
  315. throw new ServerException(_m('Lost our file data.'));
  316. }
  317. $file_d = min($filedata['width'], $filedata['height']);
  318. $dest_x = $this->arg('avatar_crop_x') ? $this->arg('avatar_crop_x') : 0;
  319. $dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y') : 0;
  320. $dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w') : $file_d;
  321. $dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h') : $file_d;
  322. $size = (int)(min($dest_w, $dest_h, common_config('avatar', 'maxsize')));
  323. $box = ['width' => $size, 'height' => $size,
  324. 'x' => $dest_x, 'y' => $dest_y,
  325. 'w' => $dest_w, 'h' => $dest_h,];
  326. $imagefile = new ImageFile($filedata['filepath'], -1);
  327. $filename = Avatar::filename(
  328. $this->scoped->getID(),
  329. image_type_to_extension($imagefile->preferredType()),
  330. $size,
  331. common_timestamp()
  332. );
  333. try {
  334. $imagefile->resizeTo(Avatar::path($filename), $box);
  335. } catch (UseFileAsThumbnailException $e) {
  336. common_debug('Using uploaded avatar directly without resizing, copying it to: ' . $filename);
  337. if (!copy($filedata['filepath'], Avatar::path($filename))) {
  338. common_debug('Tried to copy image file ' . $filedata['filepath'] . ' to destination ' . Avatar::path($filename));
  339. throw new ServerException('Could not copy file to destination.');
  340. }
  341. }
  342. if ($this->scoped->setOriginal($filename)) {
  343. @unlink($filedata['filepath']);
  344. unset($_SESSION['FILEDATA']);
  345. $this->mode = 'upload';
  346. // TRANS: Success message for having updated a user avatar.
  347. return _m('Avatar updated.');
  348. }
  349. // TRANS: Error displayed on the avatar upload page if the avatar could not be updated for an unknown reason.
  350. throw new ServerException(_m('Failed updating avatar.'));
  351. }
  352. /**
  353. * Get rid of the current avatar.
  354. *
  355. * @return string
  356. * @throws Exception
  357. *
  358. */
  359. public function deleteAvatar()
  360. {
  361. Avatar::deleteFromProfile($this->scoped);
  362. // TRANS: Success message for deleting a user avatar.
  363. return _m('Avatar deleted.');
  364. }
  365. /**
  366. * Add the jCrop stylesheet
  367. *
  368. * @return void
  369. */
  370. public function showStylesheets()
  371. {
  372. parent::showStylesheets();
  373. $this->cssLink('js/extlib/jquery-jcrop/css/jcrop.css', 'base', 'screen, projection, tv');
  374. }
  375. /**
  376. * Add the jCrop scripts
  377. *
  378. * @return void
  379. */
  380. public function showScripts()
  381. {
  382. parent::showScripts();
  383. if ($this->mode == 'crop') {
  384. $this->script('extlib/jquery-jcrop/jcrop.js');
  385. $this->script('jcrop.go.js');
  386. }
  387. $this->autofocus('avatarfile');
  388. }
  389. }