1<?php 2/** 3 * DokuWiki Avatar Plugin: displays avatar images with syntax, see: 4 * <https://www.dokuwiki.org/plugin:avatar>. 5 * 6 * Copyright (C) 2005-2007 by Esther Brunner <wikidesign@gmail.com> 7 * Copyright (C) 2008-2009 by Gina Häußge, Michael Klier <dokuwiki@chimeric.de> 8 * Copyright (C) 2013 by Michael Hamann <michael@content-space.de> 9 * Copyright (C) 2023 by Daniel Dias Rodrigues <danieldiasr@gmail.com> 10 * 11 * This program is free software; you can redistribute it and/or modify 12 * it under the terms of the GNU General Public License as published by 13 * the Free Software Foundation; either version 2 of the License, or 14 * (at your option) any later version. 15 * 16 * This program is distributed in the hope that it will be useful, 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 * GNU General Public License for more details. 20 * 21 * You should have received a copy of the GNU General Public License along 22 * with this program; if not, see <https://www.gnu.org/licenses/>. 23 */ 24 25declare(strict_types=1); 26 27if (!defined('DOKU_INC')) die(); 28 29class helper_plugin_avatar extends DokuWiki_Plugin 30{ 31 private const ALLOWED_FORMATS = ['.png', '.jpg', '.gif', '.webp']; 32 private const GRAVATAR_BASE = 'https://secure.gravatar.com/avatar/'; 33 34 private array $avatarCache = []; 35 36 public function getMethods(): array 37 { 38 return [ 39 [ 40 'name' => 'renderXhtml', 41 'desc' => 'Returns the XHTML to display an avatar', 42 'params' => [ 43 'user' => 'string|array', 44 'title' => 'string', 45 'align' => 'string', 46 'size' => 'int' 47 ], 48 'return' => ['xhtml' => 'string'] 49 ] 50 ]; 51 } 52 53 /** 54 * Renders the avatar as XHTML <img> 55 */ 56 public function renderXhtml(string|array $user, string $title = '', ?string $align = '', ?int $size = null): string 57 { 58 $src = $this->resolveAvatarUrl($user, $title, $size); 59 60 $title = hsc($title); 61 62 return '<img src="' . $src . '" ' . 63 'class="media' . $align . ' photo fn" ' . 64 'title="' . $title . '" ' . 65 'alt="' . $title . '" ' . 66 'width="' . (string) $size . '" ' . 67 'height="' . (string) $size . '" />'; 68 } 69 70 /** 71 * Gets or generates the avatar URL for a user/email 72 */ 73 public function resolveAvatarUrl(string|array $user, string &$title, int &$size): string 74 { 75 $cacheKey = $this->getCacheKey($user, $size); 76 77 if (isset($this->avatarCache[$cacheKey])) { 78 return $this->avatarCache[$cacheKey]; 79 } 80 81 $mail = $this->extractUserData($user, $title); 82 $isEmail = mail_isvalid($mail) && (!is_array($user) || !isset($user['user'])); 83 84 // For emails (Gravatar) 85 if ($isEmail) { 86 $src = $this->getGravatarUrl($mail, $size); 87 } 88 // For local users 89 else { 90 $src = $this->tryLocalAvatar($user, $title, $size); 91 92 if (!$src) { 93 // Apply fallback configured for local users only 94 if ($this->getConf('local_default') === 'monsterid' && function_exists('imagecreatetruecolor')) { 95 $seed = md5(dokuwiki\Utf8\PhpString::strtolower(is_array($user) ? ($user['user'] ?? '') : $user)); 96 $src = $this->getMonsterIdUrl($seed, $size); 97 } else { 98 $src = $this->getDefaultImageUrl($size); 99 } 100 } 101 } 102 103 if (empty($title)) { 104 $title = obfuscate($mail); 105 } 106 107 $this->avatarCache[$cacheKey] = $src; 108 return $src; 109 } 110 111 private function resolveTokens(string $template): string 112 { 113 global $INFO; 114 115 $vars = [ 116 '@USER@' => cleanID($INFO['client'] ?? ''), 117 ]; 118 119 return strtr($template, $vars); 120 } 121 122 private function getCacheKey(string|array $user, int $size): string 123 { 124 $userKey = is_array($user) ? ($user['mail'] ?? ($user['user'] ?? '')) : $user; 125 return md5($userKey . $size); 126 } 127 128 private function extractUserData(string|array $user, ?string &$title): string 129 { 130 if (is_array($user)) { 131 if (empty($title) && !empty($user['name'])) { 132 $title = hsc($user['name']); 133 } 134 return $user['mail'] ?? ''; 135 } 136 return $user; 137 } 138 139 private function tryLocalAvatar(string|array $user, ?string &$title, int $size): ?string 140 { 141 global $auth; 142 143 $username = is_array($user) ? ($user['user'] ?? '') : $user; 144 $userinfo = $auth->getUserData($username); 145 146 if (!$userinfo) return null; 147 if (empty($title) && !empty($userinfo['name'])) $title = hsc($userinfo['name']); 148 149 $ns = $this->resolveTokens($this->getConf('namespace')); 150 $existingFiles = []; 151 152 // Scan all allowed formats. 153 foreach (self::ALLOWED_FORMATS as $format) { 154 $imagePath = $ns . ':' . $username . $format; 155 $imageFile = mediaFN($imagePath); 156 157 if (file_exists($imageFile)) { 158 $existingFiles[$imagePath] = filesize($imageFile); 159 } 160 } 161 162 if ($existingFiles) { 163 // Returns the file with the smallest size. 164 asort($existingFiles); 165 $bestPath = key($existingFiles); 166 return ml($bestPath, ['w' => $size, 'h' => $size], true, '&', false); 167 } 168 169 // No local files found → generate MonsterID if it's the selected 170 // fallback 171 if ($this->getConf('local_default') === 'monsterid') { 172 if ($this->saveMonsterIdAvatar($username, 120)) { 173 $monsterPath = $ns . ':' . $username . '.png'; 174 if (file_exists(mediaFN($monsterPath))) { 175 return ml($monsterPath, ['w' => $size, 'h' => $size], true, '&', false); 176 } 177 } 178 } 179 180 return null; 181 } 182 183 private function saveMonsterIdAvatar(string $username, int $size): bool 184 { 185 global $INFO; 186 187 $currentUser = cleanID($INFO['client'] ?? ''); 188 $username = cleanID($username); 189 190 // It only allows you to save your own avatar. 191 if ($username !== $currentUser) { 192 return false; 193 } 194 195 $ns = $this->resolveTokens($this->getConf('namespace')); 196 $filename = $ns . ':' . $username . '.png'; 197 $filepath = mediaFN($filename); 198 199 // If any local avatar of the user already exists, it will not generate 200 // a MonsterID. 201 foreach (self::ALLOWED_FORMATS as $format) { 202 if (file_exists(mediaFN($ns . ':' . $username . $format))) { 203 return true; 204 } 205 } 206 207 // MonsterID URL for the user 208 $seed = md5(dokuwiki\Utf8\PhpString::strtolower($username)); 209 $monsterUrl = DOKU_URL . 'lib/plugins/avatar/monsterid.php?seed=' . $seed . '&size=' . $size; 210 211 // Download the image using file_get_contents 212 $imageData = @file_get_contents($monsterUrl); 213 if ($imageData === false) return false; 214 215 // creates the directory if it does not exist 216 io_makeFileDir($filepath); 217 218 // Save the image 219 return file_put_contents($filepath, $imageData) !== false; 220 } 221 222 private function getGravatarUrl(string $mail, int $size): string 223 { 224 $seed = md5(dokuwiki\Utf8\PhpString::strtolower($mail)); 225 226 $default = function_exists('imagecreatetruecolor') 227 ? $this->getMonsterIdUrl($seed, $size) 228 : $this->getDefaultImageUrl($size); 229 230 if (!mail_isvalid($mail)) { 231 return $default; 232 } 233 234 $params = [ 235 's' => $size, 236 'r' => $this->getConf('rating') 237 ]; 238 239 $gravatar_default = $this->getConf('gravatar_default'); 240 241 if ($gravatar_default !== 'default') { 242 $params['d'] = $gravatar_default; 243 } 244 245 return self::GRAVATAR_BASE . $seed . '?' . http_build_query($params); 246 } 247 248 private function getMonsterIdUrl(string $seed, int $size): string 249 { 250 $file = 'monsterid.php?seed=' . $seed . '&size=' . $size; 251 return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 252 } 253 254 private function getDefaultImageUrl(int $size): string 255 { 256 $file = 'images/default_' . $size . '.png'; 257 return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 258 } 259} 260