Avatar.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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. defined('GNUSOCIAL') || die();
  17. /**
  18. * Table Definition for avatar
  19. */
  20. class Avatar extends Managed_DataObject
  21. {
  22. public $__table = 'avatar'; // table name
  23. public $profile_id; // int(4) primary_key not_null
  24. public $original; // bool default_false
  25. public $width; // int(4) primary_key not_null
  26. public $height; // int(4) primary_key not_null
  27. public $mediatype; // varchar(32) not_null
  28. public $filename; // varchar(191) not 255 because utf8mb4 takes more space
  29. public $created; // datetime()
  30. public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
  31. public static function schemaDef()
  32. {
  33. return array(
  34. 'fields' => array(
  35. 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),
  36. 'original' => array('type' => 'bool', 'default' => false, 'description' => 'uploaded by user or generated?'),
  37. 'width' => array('type' => 'int', 'not null' => true, 'description' => 'image width'),
  38. 'height' => array('type' => 'int', 'not null' => true, 'description' => 'image height'),
  39. 'mediatype' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'file type'),
  40. 'filename' => array('type' => 'varchar', 'length' => 191, 'description' => 'local filename, if local'),
  41. 'created' => array('type' => 'datetime', 'description' => 'date this record was created'),
  42. 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
  43. ),
  44. 'primary key' => array('profile_id', 'width', 'height'),
  45. 'unique keys' => array(
  46. // 'avatar_filename_key' => array('filename'),
  47. ),
  48. 'foreign keys' => array(
  49. 'avatar_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
  50. ),
  51. );
  52. }
  53. // We clean up the file, too
  54. public function delete($useWhere = false)
  55. {
  56. $filename = $this->filename;
  57. if (file_exists(Avatar::path($filename))) {
  58. @unlink(Avatar::path($filename));
  59. }
  60. return parent::delete($useWhere);
  61. }
  62. /*
  63. * Deletes all avatars (but may spare the original) from a profile.
  64. *
  65. * @param Profile $target The profile we're deleting avatars of.
  66. * @param boolean $original Whether original should be removed or not.
  67. */
  68. public static function deleteFromProfile(Profile $target, $original = true)
  69. {
  70. try {
  71. $avatars = self::getProfileAvatars($target);
  72. foreach ($avatars as $avatar) {
  73. if ($avatar->original && !$original) {
  74. continue;
  75. }
  76. $avatar->delete();
  77. }
  78. } catch (NoAvatarException $e) {
  79. // There are no avatars to delete, a sort of success.
  80. }
  81. return true;
  82. }
  83. protected static $_avatars = [];
  84. /*
  85. * Get an avatar by profile. Currently can't call newSize with $height
  86. */
  87. public static function byProfile(Profile $target, $width=null, $height=null)
  88. {
  89. $width = intval($width);
  90. $height = !is_null($height) ? intval($height) : null;
  91. if (is_null($height)) {
  92. $height = $width;
  93. }
  94. $size = "{$width}x{$height}";
  95. if (!isset(self::$_avatars[$target->id])) {
  96. self::$_avatars[$target->id] = array();
  97. } elseif (isset(self::$_avatars[$target->id][$size])) {
  98. return self::$_avatars[$target->id][$size];
  99. }
  100. $avatar = null;
  101. if (Event::handle('StartProfileGetAvatar', array($target, $width, &$avatar))) {
  102. $avatar = self::pkeyGet(
  103. array(
  104. 'profile_id' => $target->id,
  105. 'width' => $width,
  106. 'height' => $height,
  107. )
  108. );
  109. Event::handle('EndProfileGetAvatar', array($target, $width, &$avatar));
  110. }
  111. if (is_null($avatar)) {
  112. // Obviously we can't find an avatar, so let's resize the original!
  113. $avatar = Avatar::newSize($target, $width);
  114. } elseif (!($avatar instanceof Avatar)) {
  115. throw new NoAvatarException($target, $avatar);
  116. }
  117. self::$_avatars[$target->id]["{$avatar->width}x{$avatar->height}"] = $avatar;
  118. return $avatar;
  119. }
  120. public static function getUploaded(Profile $target)
  121. {
  122. $avatar = new Avatar();
  123. $avatar->profile_id = $target->id;
  124. $avatar->original = true;
  125. if (!$avatar->find(true)) {
  126. throw new NoAvatarException($target, $avatar);
  127. }
  128. if (!file_exists(Avatar::path($avatar->filename))) {
  129. // The delete call may be odd for, say, unmounted filesystems
  130. // that cause a file to currently not exist, but actually it does...
  131. $avatar->delete();
  132. throw new NoAvatarException($target, $avatar);
  133. }
  134. return $avatar;
  135. }
  136. public static function getProfileAvatars(Profile $target)
  137. {
  138. $avatar = new Avatar();
  139. $avatar->profile_id = $target->id;
  140. if (!$avatar->find()) {
  141. throw new NoAvatarException($target, $avatar);
  142. }
  143. return $avatar->fetchAll();
  144. }
  145. /**
  146. * Where should the avatar go for this user?
  147. * @param int $id user id
  148. * @param string $extension file extension
  149. * @param int|null $size file size
  150. * @param string|null $extra extra bit for the filename
  151. * @return string
  152. */
  153. public static function filename(int $id, string $extension, ?int $size = null, ?string $extra = null)
  154. {
  155. if ($size) {
  156. return $id . '-' . $size . (($extra) ? ('-' . $extra) : '') . $extension;
  157. } else {
  158. return $id . '-original' . (($extra) ? ('-' . $extra) : '') . $extension;
  159. }
  160. }
  161. public static function path($filename)
  162. {
  163. $dir = common_config('avatar', 'dir');
  164. if ($dir[strlen($dir)-1] != '/') {
  165. $dir .= '/';
  166. }
  167. return $dir . $filename;
  168. }
  169. public static function url($filename)
  170. {
  171. $path = common_config('avatar', 'url_base');
  172. if ($path[strlen($path)-1] != '/') {
  173. $path .= '/';
  174. }
  175. if ($path[0] != '/') {
  176. $path = '/'.$path;
  177. }
  178. $server = common_config('avatar', 'server');
  179. if (empty($server)) {
  180. $server = common_config('site', 'server');
  181. }
  182. $ssl = (common_config('avatar', 'ssl') || GNUsocial::useHTTPS());
  183. $protocol = ($ssl) ? 'https' : 'http';
  184. return $protocol.'://'.$server.$path.$filename;
  185. }
  186. public function displayUrl()
  187. {
  188. return Avatar::url($this->filename);
  189. }
  190. public static function urlByProfile(Profile $target, $width = null, $height = null)
  191. {
  192. try {
  193. return self::byProfile($target, $width, $height)->displayUrl();
  194. } catch (Exception $e) {
  195. return self::defaultImage($width);
  196. }
  197. }
  198. public static function defaultImage($size = null)
  199. {
  200. if (is_null($size)) {
  201. $size = AVATAR_PROFILE_SIZE;
  202. }
  203. static $sizenames = array(AVATAR_PROFILE_SIZE => 'profile',
  204. AVATAR_STREAM_SIZE => 'stream',
  205. AVATAR_MINI_SIZE => 'mini');
  206. return Theme::path('default-avatar-'.$sizenames[$size].'.png');
  207. }
  208. public static function newSize(Profile $target, $width)
  209. {
  210. $width = intval($width);
  211. if ($width < 1 || $width > common_config('avatar', 'maxsize')) {
  212. // TRANS: An error message when avatar size is unreasonable
  213. throw new Exception(_m('Avatar size too large'));
  214. }
  215. // So far we only have square avatars and I don't have time to
  216. // rewrite support for non-square ones right now ;)
  217. $height = $width;
  218. $original = Avatar::getUploaded($target);
  219. $imagefile = new ImageFile(null, Avatar::path($original->filename));
  220. $filename = Avatar::filename(
  221. $target->getID(),
  222. image_type_to_extension($imagefile->preferredType()),
  223. $width,
  224. common_timestamp()
  225. );
  226. $imagefile->resizeTo(Avatar::path($filename), array('width'=>$width, 'height'=>$height));
  227. $scaled = clone($original);
  228. $scaled->original = false;
  229. $scaled->width = $width;
  230. $scaled->height = $height;
  231. $scaled->filename = $filename;
  232. $scaled->created = common_sql_now();
  233. if (!$scaled->insert()) {
  234. // TRANS: An error message when unable to insert avatar data into the db
  235. throw new Exception(_m('Could not insert new avatar data to database'));
  236. }
  237. // Return the new avatar object
  238. return $scaled;
  239. }
  240. }