WebOrbiton
v1.0.0.1

StocketBase

131 lines · 4.3 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. final class Avatar
  6. {
  7. private const DIR = 'media/avatars';
  8. private const MAX_BYTES = 2 * 1024 * 1024;
  9. private const MAX_DIMENSION = 6000;
  10. private const SIZE = 256;
  11. private const TYPES = [
  12. 'image/jpeg' => 'jpg',
  13. 'image/png' => 'png',
  14. 'image/webp' => 'webp',
  15. ];
  16. ​
  17. public static function hasUpload(?array $file): bool
  18. {
  19. return $file !== null && ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE;
  20. }
  21. ​
  22. public static function store(array $file): array
  23. {
  24. if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
  25. return [null, 'Photo upload failed.'];
  26. }
  27. ​
  28. if ((int) $file['size'] > self::MAX_BYTES) {
  29. return [null, 'Photo is too large (max 2MB).'];
  30. }
  31. ​
  32. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  33. $mimeType = (string) finfo_file($finfo, $file['tmp_name']);
  34. ​
  35. $imageInfo = @getimagesize($file['tmp_name']);
  36. if (!isset(self::TYPES[$mimeType]) || $imageInfo === false) {
  37. return [null, 'Photo must be a JPG, PNG or WebP image.'];
  38. }
  39. ​
  40. if ($imageInfo[0] > self::MAX_DIMENSION || $imageInfo[1] > self::MAX_DIMENSION) {
  41. return [null, 'Photo dimensions are too large (max ' . self::MAX_DIMENSION . ' px).'];
  42. }
  43. ​
  44. $directory = dirname(__DIR__) . '/' . self::DIR;
  45. if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) {
  46. return [null, 'Could not create the photos folder.'];
  47. }
  48. ​
  49. $baseName = bin2hex(random_bytes(16));
  50. $extension = self::resize($file['tmp_name'], $directory . '/' . $baseName);
  51. ​
  52. if ($extension === null) {
  53. $extension = self::TYPES[$mimeType];
  54. if (!move_uploaded_file($file['tmp_name'], $directory . '/' . $baseName . '.' . $extension)) {
  55. return [null, 'Could not save the photo.'];
  56. }
  57. }
  58. ​
  59. return [self::DIR . '/' . $baseName . '.' . $extension, null];
  60. }
  61. ​
  62. public static function delete(?string $path): void
  63. {
  64. if (!self::isValidPath($path)) {
  65. return;
  66. }
  67. ​
  68. $fullPath = dirname(__DIR__) . '/' . $path;
  69. if (is_file($fullPath)) {
  70. @unlink($fullPath);
  71. }
  72. }
  73. ​
  74. public static function html(array $account, string $class): string
  75. {
  76. $path = (string) ($account['avatar_path'] ?? '');
  77. ​
  78. if (self::isValidPath($path)) {
  79. return '<div class="' . htmlspecialchars($class, ENT_QUOTES) . ' has-image"><img src="' . htmlspecialchars($path, ENT_QUOTES) . '" alt=""></div>';
  80. }
  81. ​
  82. return '<div class="' . htmlspecialchars($class, ENT_QUOTES) . '">' . htmlspecialchars(self::initial((string) ($account['display_name'] ?? '?'))) . '</div>';
  83. }
  84. ​
  85. public static function initial(string $name): string
  86. {
  87. return mb_strtoupper(mb_substr($name !== '' ? $name : '?', 0, 1));
  88. }
  89. ​
  90. public static function isValidPath(?string $path): bool
  91. {
  92. return $path !== null
  93. && $path !== ''
  94. && str_starts_with($path, self::DIR . '/')
  95. && !str_contains($path, '..');
  96. }
  97. ​
  98. private static function resize(string $source, string $targetBase): ?string
  99. {
  100. if (!function_exists('imagecreatefromstring') || !function_exists('imagecreatetruecolor')) {
  101. return null;
  102. }
  103. ​
  104. $data = @file_get_contents($source);
  105. $image = $data !== false ? @imagecreatefromstring($data) : false;
  106. if ($image === false) {
  107. return null;
  108. }
  109. ​
  110. $width = imagesx($image);
  111. $height = imagesy($image);
  112. $side = min($width, $height);
  113. ​
  114. $canvas = imagecreatetruecolor(self::SIZE, self::SIZE);
  115. imagealphablending($canvas, false);
  116. imagesavealpha($canvas, true);
  117. imagefill($canvas, 0, 0, imagecolorallocatealpha($canvas, 0, 0, 0, 127));
  118. imagecopyresampled($canvas, $image, 0, 0, intdiv($width - $side, 2), intdiv($height - $side, 2), self::SIZE, self::SIZE, $side, $side);
  119. ​
  120. if (function_exists('imagewebp') && @imagewebp($canvas, $targetBase . '.webp', 85)) {
  121. return 'webp';
  122. }
  123. ​
  124. if (@imagepng($canvas, $targetBase . '.png', 6)) {
  125. return 'png';
  126. }
  127. ​
  128. return null;
  129. }
  130. }
  131. ​