1<?php 2/** 3 * Avatar Plugin for DokuWiki 4 * 5 * Displays avatar images with syntax {{avatar>email@domain.com}} 6 * Supports local avatars, Gravatar.com, and MonsterID fallback 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Esther Brunner <wikidesign@gmail.com> 10 * @author Daniel Dias Rodrigues <danieldiasr@gmail.com> (modernization) 11 */ 12 13declare(strict_types=1); 14 15if (!defined('DOKU_INC')) die(); 16 17class helper_plugin_avatar extends DokuWiki_Plugin 18{ 19 private const DEFAULT_SIZES = [ 20 'small' => 20, 21 'medium' => 40, 22 'large' => 80, 23 'xlarge' => 120 24 ]; 25 26 private const ALLOWED_FORMATS = ['.png', '.jpg', '.gif']; 27 private const GRAVATAR_BASE = 'https://secure.gravatar.com/avatar/'; 28 29 private array $avatarCache = []; 30 31 public function getMethods(): array 32 { 33 return [ 34 [ 35 'name' => 'renderXhtml', 36 'desc' => 'Returns the XHTML to display an avatar', 37 'params' => [ 38 'user' => 'string|array', 39 'title' => 'string', 40 'align' => 'string', 41 'size' => 'int' 42 ], 43 'return' => ['xhtml' => 'string'] 44 ] 45 ]; 46 } 47 48 /** 49 * Renders the avatar as XHTML <img> 50 */ 51 public function renderXhtml(string|array $user, string $title = '', ?string $align = '', ?int $size = null): string 52 { 53 $src = $this->resolveAvatarUrl($user, $title, $size); 54 55 return '<img src="' . $src . '" ' . 56 'class="media' . $align . ' photo fn" ' . 57 'title="' . $title . '" ' . 58 'alt="' . $title . '" ' . 59 'width="' . (string) $size . '" ' . 60 'height="' . (string) $size . '" />'; 61 } 62 63 /** 64 * Gets or generates the avatar URL for a user/email 65 */ 66 public function resolveAvatarUrl(string|array $user, ?string &$title = null, ?int &$size = null): string 67 { 68 global $auth; 69 70 $size = $this->normalizeSize($size); 71 $cacheKey = $this->getCacheKey($user, $title, $size); 72 73 if (isset($this->avatarCache[$cacheKey])) { 74 return $this->avatarCache[$cacheKey]; 75 } 76 77 $mail = $this->extractUserData($user, $title); 78 $src = $this->tryLocalAvatar($user, $title, $size); 79 80 if (!$src) { 81 $src = $this->getGravatarUrl($mail, $size); 82 } 83 84 if (empty($title)) { 85 $title = obfuscate($mail); 86 } 87 88 $this->avatarCache[$cacheKey] = $src; 89 return $src; 90 } 91 92 private function normalizeSize(?int $size): int 93 { 94 if ($size && $size > 0) { 95 return $size; 96 } 97 98 $confSize = (int) $this->getConf('size'); 99 return $confSize > 0 ? $confSize : self::DEFAULT_SIZES['medium']; 100 } 101 102 private function getCacheKey(string|array $user, ?string $title, int $size): string 103 { 104 $userKey = is_array($user) ? ($user['mail'] ?? '') : $user; 105 return md5($userKey . $title . $size); 106 } 107 108 private function extractUserData(string|array $user, ?string &$title): string 109 { 110 if (is_array($user)) { 111 if (empty($title) && !empty($user['name'])) { 112 $title = hsc($user['name']); 113 } 114 return $user['mail'] ?? ''; 115 } 116 return $user; 117 } 118 119 private function tryLocalAvatar(string|array $user, ?string &$title, int $size): ?string 120 { 121 global $auth; 122 123 $username = is_array($user) ? ($user['user'] ?? '') : $user; 124 $userinfo = $auth->getUserData($username); 125 126 if (!$userinfo) { 127 return null; 128 } 129 130 if (empty($title) && !empty($userinfo['name'])) { 131 $title = hsc($userinfo['name']); 132 } 133 134 $ns = $this->getConf('namespace'); 135 foreach (self::ALLOWED_FORMATS as $format) { 136 $imagePath = $ns . ':' . $username . $format; 137 $imageFile = mediaFN($imagePath); 138 139 if (file_exists($imageFile)) { 140 return ml($imagePath, ['w' => $size, 'h' => $size], true, '&', false); 141 } 142 } 143 144 // If it is the user itself, it generates and saves Monsterid 145 if (is_string($user) && $user === $username) { 146 if ($this->saveMonsterIdAvatar($username, 120)) { // Save large size to quality 147 $imagePath = $ns . ':' . $username . '.png'; 148 if (file_exists(mediaFN($imagePath))) { 149 return ml($imagePath, ['w' => $size, 'h' => $size], true, '&', false); 150 } 151 } 152 } 153 154 return null; 155 } 156 157 private function saveMonsterIdAvatar(string $username, int $size): bool 158 { 159 $ns = $this->getConf('namespace'); 160 $filename = $ns . ':' . $username . '.png'; 161 $filepath = mediaFN($filename); 162 163 // Monsterid URL for the user 164 $seed = md5(dokuwiki\Utf8\PhpString::strtolower($username)); 165 $monsterUrl = DOKU_URL . 'lib/plugins/avatar/monsterid.php?seed=' . $seed . '&size=' . $size; 166 167 // Download the image using file_get_contents 168 $imageData = @file_get_contents($monsterUrl); 169 if ($imageData === false) { 170 return false; 171 } 172 173 // creates the directory if it does not exist 174 io_makeFileDir($filepath); 175 176 // Save the image 177 return file_put_contents($filepath, $imageData) !== false; 178 } 179 180 private function getGravatarUrl(string $mail, int $size): string 181 { 182 $seed = md5(dokuwiki\Utf8\PhpString::strtolower($mail)); 183 184 $default = function_exists('imagecreatetruecolor') 185 ? $this->getMonsterIdUrl($seed, $size) 186 : $this->getDefaultImageUrl($size); 187 188 if (!mail_isvalid($mail)) { 189 return $default; 190 } 191 192 $params = [ 193 's' => $size, 194 'd' => $this->getConf('default'), 195 'r' => $this->getConf('rating') 196 ]; 197 198 return self::GRAVATAR_BASE . $seed . '?' . http_build_query($params); 199 } 200 201 private function getMonsterIdUrl(string $seed, int $size): string 202 { 203 $file = 'monsterid.php?seed=' . $seed . '&size=' . $size . '&.png'; 204 return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 205 } 206 207 private function getDefaultImageUrl(int $size): string 208 { 209 $validSizes = array_values(self::DEFAULT_SIZES); 210 $realSize = in_array($size, $validSizes, true) ? $size : self::DEFAULT_SIZES['xlarge']; 211 $file = 'images/default_' . $realSize . '.png'; 212 return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 213 } 214} 215 216