132c37733Swikidesign<?php 232c37733Swikidesign/** 323415f0dSnerun * Avatar Plugin for DokuWiki 423415f0dSnerun * 523415f0dSnerun * Displays avatar images with syntax {{avatar>email@domain.com}} 623415f0dSnerun * Supports local avatars, Gravatar.com, and MonsterID fallback 723415f0dSnerun * 832c37733Swikidesign * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 932c37733Swikidesign * @author Esther Brunner <wikidesign@gmail.com> 1023415f0dSnerun * @author Daniel Dias Rodrigues <danieldiasr@gmail.com> (modernization) 1132c37733Swikidesign */ 1232c37733Swikidesign 1323415f0dSnerundeclare(strict_types=1); 1423415f0dSnerun 1532c37733Swikidesignif (!defined('DOKU_INC')) die(); 1632c37733Swikidesign 1723415f0dSnerunclass helper_plugin_avatar extends DokuWiki_Plugin 1823415f0dSnerun{ 1985db55c8Snerun private const ALLOWED_FORMATS = ['.png', '.jpg', '.gif', '.webp']; 2023415f0dSnerun private const GRAVATAR_BASE = 'https://secure.gravatar.com/avatar/'; 2123415f0dSnerun 2223415f0dSnerun private array $avatarCache = []; 2323415f0dSnerun 2423415f0dSnerun public function getMethods(): array 2523415f0dSnerun { 2623415f0dSnerun return [ 2723415f0dSnerun [ 2823415f0dSnerun 'name' => 'renderXhtml', 2923415f0dSnerun 'desc' => 'Returns the XHTML to display an avatar', 3023415f0dSnerun 'params' => [ 3123415f0dSnerun 'user' => 'string|array', 3223415f0dSnerun 'title' => 'string', 3323415f0dSnerun 'align' => 'string', 3423415f0dSnerun 'size' => 'int' 3523415f0dSnerun ], 3623415f0dSnerun 'return' => ['xhtml' => 'string'] 3723415f0dSnerun ] 3823415f0dSnerun ]; 3932c37733Swikidesign } 4032c37733Swikidesign 411bc20383SMichael Klier /** 4223415f0dSnerun * Renders the avatar as XHTML <img> 431bc20383SMichael Klier */ 44986bb475Snerun public function renderXhtml(string|array $user, string $title = '', ?string $align = '', ?int $size = null): string 4523415f0dSnerun { 4623415f0dSnerun $src = $this->resolveAvatarUrl($user, $title, $size); 4732c37733Swikidesign 48f994dce8Snerun $title = hsc($title); 49f994dce8Snerun 5054eb7e18Snerun return '<img src="' . $src . '" ' . 5154eb7e18Snerun 'class="media' . $align . ' photo fn" ' . 5254eb7e18Snerun 'title="' . $title . '" ' . 5354eb7e18Snerun 'alt="' . $title . '" ' . 5454eb7e18Snerun 'width="' . (string) $size . '" ' . 5554eb7e18Snerun 'height="' . (string) $size . '" />'; 5632c37733Swikidesign } 5732c37733Swikidesign 5832c37733Swikidesign /** 5923415f0dSnerun * Gets or generates the avatar URL for a user/email 6032c37733Swikidesign */ 61*4cc391acSnerun public function resolveAvatarUrl(string|array $user, string &$title, int &$size): string 6223415f0dSnerun { 63f2a729a9Snerun $cacheKey = $this->getCacheKey($user, $size); 6432c37733Swikidesign 6523415f0dSnerun if (isset($this->avatarCache[$cacheKey])) { 6623415f0dSnerun return $this->avatarCache[$cacheKey]; 671bc20383SMichael Klier } 681bc20383SMichael Klier 6923415f0dSnerun $mail = $this->extractUserData($user, $title); 704c5c7bddSnerun $isEmail = mail_isvalid($mail) && (!is_array($user) || !isset($user['user'])); 714c5c7bddSnerun 724c5c7bddSnerun // For emails (Gravatar) 734c5c7bddSnerun if ($isEmail) { 744c5c7bddSnerun $src = $this->getGravatarUrl($mail, $size); 754c5c7bddSnerun } 764c5c7bddSnerun // For local users 774c5c7bddSnerun else { 7823415f0dSnerun $src = $this->tryLocalAvatar($user, $title, $size); 7923415f0dSnerun 8023415f0dSnerun if (!$src) { 814c5c7bddSnerun // Apply fallback configured for local users only 824c5c7bddSnerun if ($this->getConf('local_default') === 'monsterid' && function_exists('imagecreatetruecolor')) { 834c5c7bddSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower(is_array($user) ? ($user['user'] ?? '') : $user)); 844c5c7bddSnerun $src = $this->getMonsterIdUrl($seed, $size); 854c5c7bddSnerun } else { 864c5c7bddSnerun $src = $this->getDefaultImageUrl($size); 874c5c7bddSnerun } 884c5c7bddSnerun } 8932c37733Swikidesign } 9032c37733Swikidesign 9123415f0dSnerun if (empty($title)) { 9223415f0dSnerun $title = obfuscate($mail); 9332c37733Swikidesign } 9432c37733Swikidesign 9523415f0dSnerun $this->avatarCache[$cacheKey] = $src; 9632c37733Swikidesign return $src; 9732c37733Swikidesign } 9823415f0dSnerun 99f2a729a9Snerun private function getCacheKey(string|array $user, int $size): string 10023415f0dSnerun { 101f2a729a9Snerun $userKey = is_array($user) ? ($user['mail'] ?? ($user['user'] ?? '')) : $user; 102f2a729a9Snerun return md5($userKey . $size); 10323415f0dSnerun } 10423415f0dSnerun 10523415f0dSnerun private function extractUserData(string|array $user, ?string &$title): string 10623415f0dSnerun { 10723415f0dSnerun if (is_array($user)) { 10823415f0dSnerun if (empty($title) && !empty($user['name'])) { 10923415f0dSnerun $title = hsc($user['name']); 11023415f0dSnerun } 11123415f0dSnerun return $user['mail'] ?? ''; 11223415f0dSnerun } 11323415f0dSnerun return $user; 11423415f0dSnerun } 11523415f0dSnerun 11623415f0dSnerun private function tryLocalAvatar(string|array $user, ?string &$title, int $size): ?string 11723415f0dSnerun { 11823415f0dSnerun global $auth; 11923415f0dSnerun 12023415f0dSnerun $username = is_array($user) ? ($user['user'] ?? '') : $user; 12123415f0dSnerun $userinfo = $auth->getUserData($username); 12223415f0dSnerun 12385db55c8Snerun if (!$userinfo) return null; 12485db55c8Snerun if (empty($title) && !empty($userinfo['name'])) $title = hsc($userinfo['name']); 12523415f0dSnerun 12623415f0dSnerun $ns = $this->getConf('namespace'); 12785db55c8Snerun $existingFiles = []; 12885db55c8Snerun 12985db55c8Snerun // Scan all allowed formats. 13023415f0dSnerun foreach (self::ALLOWED_FORMATS as $format) { 13123415f0dSnerun $imagePath = $ns . ':' . $username . $format; 13223415f0dSnerun $imageFile = mediaFN($imagePath); 13323415f0dSnerun 13423415f0dSnerun if (file_exists($imageFile)) { 13585db55c8Snerun $existingFiles[$imagePath] = filesize($imageFile); 13623415f0dSnerun } 13723415f0dSnerun } 13823415f0dSnerun 13985db55c8Snerun if ($existingFiles) { 14085db55c8Snerun // Returns the file with the smallest size. 14185db55c8Snerun asort($existingFiles); 14285db55c8Snerun $bestPath = key($existingFiles); 14385db55c8Snerun return ml($bestPath, ['w' => $size, 'h' => $size], true, '&', false); 144f443324cSnerun } 14585db55c8Snerun 14685db55c8Snerun // No local files found → generate MonsterID if it's the selected 14785db55c8Snerun // fallback 14885db55c8Snerun if ($this->getConf('local_default') === 'monsterid') { 14985db55c8Snerun if ($this->saveMonsterIdAvatar($username, 120)) { 15085db55c8Snerun $monsterPath = $ns . ':' . $username . '.png'; 15185db55c8Snerun if (file_exists(mediaFN($monsterPath))) { 15285db55c8Snerun return ml($monsterPath, ['w' => $size, 'h' => $size], true, '&', false); 153f443324cSnerun } 154f443324cSnerun } 1554c5c7bddSnerun } 156f443324cSnerun 15723415f0dSnerun return null; 15823415f0dSnerun } 15923415f0dSnerun 160f443324cSnerun private function saveMonsterIdAvatar(string $username, int $size): bool 161f443324cSnerun { 162f443324cSnerun $ns = $this->getConf('namespace'); 163f443324cSnerun $filename = $ns . ':' . $username . '.png'; 164f443324cSnerun $filepath = mediaFN($filename); 165f443324cSnerun 16685db55c8Snerun // If any local avatar of the user already exists, it will not generate 16785db55c8Snerun // a MonsterID. 16885db55c8Snerun foreach (self::ALLOWED_FORMATS as $format) { 16985db55c8Snerun if (file_exists(mediaFN($ns . ':' . $username . $format))) { 170f2a729a9Snerun return true; 171f2a729a9Snerun } 17285db55c8Snerun } 173f2a729a9Snerun 17485db55c8Snerun // MonsterID URL for the user 175f443324cSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower($username)); 176f443324cSnerun $monsterUrl = DOKU_URL . 'lib/plugins/avatar/monsterid.php?seed=' . $seed . '&size=' . $size; 177f443324cSnerun 178f443324cSnerun // Download the image using file_get_contents 179f443324cSnerun $imageData = @file_get_contents($monsterUrl); 18085db55c8Snerun if ($imageData === false) return false; 181f443324cSnerun 182f443324cSnerun // creates the directory if it does not exist 183f443324cSnerun io_makeFileDir($filepath); 184f443324cSnerun 185f443324cSnerun // Save the image 186f443324cSnerun return file_put_contents($filepath, $imageData) !== false; 187f443324cSnerun } 188f443324cSnerun 18923415f0dSnerun private function getGravatarUrl(string $mail, int $size): string 19023415f0dSnerun { 19123415f0dSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower($mail)); 19223415f0dSnerun 19323415f0dSnerun $default = function_exists('imagecreatetruecolor') 19423415f0dSnerun ? $this->getMonsterIdUrl($seed, $size) 19523415f0dSnerun : $this->getDefaultImageUrl($size); 19623415f0dSnerun 19723415f0dSnerun if (!mail_isvalid($mail)) { 19823415f0dSnerun return $default; 19923415f0dSnerun } 20023415f0dSnerun 20123415f0dSnerun $params = [ 20223415f0dSnerun 's' => $size, 2034c5c7bddSnerun 'd' => $this->getConf('gravatar_default'), 20423415f0dSnerun 'r' => $this->getConf('rating') 20523415f0dSnerun ]; 20623415f0dSnerun 20754eb7e18Snerun return self::GRAVATAR_BASE . $seed . '?' . http_build_query($params); 20823415f0dSnerun } 20923415f0dSnerun 21023415f0dSnerun private function getMonsterIdUrl(string $seed, int $size): string 21123415f0dSnerun { 212f2a729a9Snerun $file = 'monsterid.php?seed=' . $seed . '&size=' . $size; 21323415f0dSnerun return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 21423415f0dSnerun } 21523415f0dSnerun 21623415f0dSnerun private function getDefaultImageUrl(int $size): string 21723415f0dSnerun { 218*4cc391acSnerun $file = 'images/default_' . $size . '.png'; 21923415f0dSnerun return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 22023415f0dSnerun } 22123415f0dSnerun} 222