132c37733Swikidesign<?php 232c37733Swikidesign/** 337ab8232Snerun * DokuWiki Avatar Plugin: displays avatar images with syntax, see: 437ab8232Snerun * <https://www.dokuwiki.org/plugin:avatar>. 523415f0dSnerun * 637ab8232Snerun * Copyright (C) 2005-2007 by Esther Brunner <wikidesign@gmail.com> 737ab8232Snerun * Copyright (C) 2008-2009 by Gina Häußge, Michael Klier <dokuwiki@chimeric.de> 837ab8232Snerun * Copyright (C) 2013 by Michael Hamann <michael@content-space.de> 937ab8232Snerun * Copyright (C) 2023 by Daniel Dias Rodrigues <danieldiasr@gmail.com> 1023415f0dSnerun * 1137ab8232Snerun * This program is free software; you can redistribute it and/or modify 1237ab8232Snerun * it under the terms of the GNU General Public License as published by 1337ab8232Snerun * the Free Software Foundation; either version 2 of the License, or 1437ab8232Snerun * (at your option) any later version. 1537ab8232Snerun * 1637ab8232Snerun * This program is distributed in the hope that it will be useful, 1737ab8232Snerun * but WITHOUT ANY WARRANTY; without even the implied warranty of 1837ab8232Snerun * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1937ab8232Snerun * GNU General Public License for more details. 2037ab8232Snerun * 2137ab8232Snerun * You should have received a copy of the GNU General Public License along 2237ab8232Snerun * with this program; if not, see <https://www.gnu.org/licenses/>. 2332c37733Swikidesign */ 2432c37733Swikidesign 2523415f0dSnerundeclare(strict_types=1); 2623415f0dSnerun 2732c37733Swikidesignif (!defined('DOKU_INC')) die(); 2832c37733Swikidesign 2923415f0dSnerunclass helper_plugin_avatar extends DokuWiki_Plugin 3023415f0dSnerun{ 3185db55c8Snerun private const ALLOWED_FORMATS = ['.png', '.jpg', '.gif', '.webp']; 3223415f0dSnerun private const GRAVATAR_BASE = 'https://secure.gravatar.com/avatar/'; 3323415f0dSnerun 3423415f0dSnerun private array $avatarCache = []; 3523415f0dSnerun 3623415f0dSnerun public function getMethods(): array 3723415f0dSnerun { 3823415f0dSnerun return [ 3923415f0dSnerun [ 4023415f0dSnerun 'name' => 'renderXhtml', 4123415f0dSnerun 'desc' => 'Returns the XHTML to display an avatar', 4223415f0dSnerun 'params' => [ 4323415f0dSnerun 'user' => 'string|array', 4423415f0dSnerun 'title' => 'string', 4523415f0dSnerun 'align' => 'string', 4623415f0dSnerun 'size' => 'int' 4723415f0dSnerun ], 4823415f0dSnerun 'return' => ['xhtml' => 'string'] 4923415f0dSnerun ] 5023415f0dSnerun ]; 5132c37733Swikidesign } 5232c37733Swikidesign 531bc20383SMichael Klier /** 5423415f0dSnerun * Renders the avatar as XHTML <img> 551bc20383SMichael Klier */ 56986bb475Snerun public function renderXhtml(string|array $user, string $title = '', ?string $align = '', ?int $size = null): string 5723415f0dSnerun { 5823415f0dSnerun $src = $this->resolveAvatarUrl($user, $title, $size); 5932c37733Swikidesign 60f994dce8Snerun $title = hsc($title); 61f994dce8Snerun 6254eb7e18Snerun return '<img src="' . $src . '" ' . 6354eb7e18Snerun 'class="media' . $align . ' photo fn" ' . 6454eb7e18Snerun 'title="' . $title . '" ' . 6554eb7e18Snerun 'alt="' . $title . '" ' . 6654eb7e18Snerun 'width="' . (string) $size . '" ' . 6754eb7e18Snerun 'height="' . (string) $size . '" />'; 6832c37733Swikidesign } 6932c37733Swikidesign 7032c37733Swikidesign /** 7123415f0dSnerun * Gets or generates the avatar URL for a user/email 7232c37733Swikidesign */ 734cc391acSnerun public function resolveAvatarUrl(string|array $user, string &$title, int &$size): string 7423415f0dSnerun { 75f2a729a9Snerun $cacheKey = $this->getCacheKey($user, $size); 7632c37733Swikidesign 7723415f0dSnerun if (isset($this->avatarCache[$cacheKey])) { 7823415f0dSnerun return $this->avatarCache[$cacheKey]; 791bc20383SMichael Klier } 801bc20383SMichael Klier 8123415f0dSnerun $mail = $this->extractUserData($user, $title); 824c5c7bddSnerun $isEmail = mail_isvalid($mail) && (!is_array($user) || !isset($user['user'])); 834c5c7bddSnerun 844c5c7bddSnerun // For emails (Gravatar) 854c5c7bddSnerun if ($isEmail) { 864c5c7bddSnerun $src = $this->getGravatarUrl($mail, $size); 874c5c7bddSnerun } 884c5c7bddSnerun // For local users 894c5c7bddSnerun else { 9023415f0dSnerun $src = $this->tryLocalAvatar($user, $title, $size); 9123415f0dSnerun 9223415f0dSnerun if (!$src) { 934c5c7bddSnerun // Apply fallback configured for local users only 944c5c7bddSnerun if ($this->getConf('local_default') === 'monsterid' && function_exists('imagecreatetruecolor')) { 954c5c7bddSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower(is_array($user) ? ($user['user'] ?? '') : $user)); 964c5c7bddSnerun $src = $this->getMonsterIdUrl($seed, $size); 974c5c7bddSnerun } else { 984c5c7bddSnerun $src = $this->getDefaultImageUrl($size); 994c5c7bddSnerun } 1004c5c7bddSnerun } 10132c37733Swikidesign } 10232c37733Swikidesign 10323415f0dSnerun if (empty($title)) { 10423415f0dSnerun $title = obfuscate($mail); 10532c37733Swikidesign } 10632c37733Swikidesign 10723415f0dSnerun $this->avatarCache[$cacheKey] = $src; 10832c37733Swikidesign return $src; 10932c37733Swikidesign } 11023415f0dSnerun 1112f9fb8e6Snerun private function resolveTokens(string $template): string 1122f9fb8e6Snerun { 1132f9fb8e6Snerun global $INFO; 1142f9fb8e6Snerun 1152f9fb8e6Snerun $vars = [ 1162f9fb8e6Snerun '@USER@' => cleanID($INFO['client'] ?? ''), 1172f9fb8e6Snerun ]; 1182f9fb8e6Snerun 1192f9fb8e6Snerun return strtr($template, $vars); 1202f9fb8e6Snerun } 1212f9fb8e6Snerun 122f2a729a9Snerun private function getCacheKey(string|array $user, int $size): string 12323415f0dSnerun { 124f2a729a9Snerun $userKey = is_array($user) ? ($user['mail'] ?? ($user['user'] ?? '')) : $user; 125f2a729a9Snerun return md5($userKey . $size); 12623415f0dSnerun } 12723415f0dSnerun 12823415f0dSnerun private function extractUserData(string|array $user, ?string &$title): string 12923415f0dSnerun { 13023415f0dSnerun if (is_array($user)) { 13123415f0dSnerun if (empty($title) && !empty($user['name'])) { 13223415f0dSnerun $title = hsc($user['name']); 13323415f0dSnerun } 13423415f0dSnerun return $user['mail'] ?? ''; 13523415f0dSnerun } 13623415f0dSnerun return $user; 13723415f0dSnerun } 13823415f0dSnerun 13923415f0dSnerun private function tryLocalAvatar(string|array $user, ?string &$title, int $size): ?string 14023415f0dSnerun { 14123415f0dSnerun global $auth; 14223415f0dSnerun 14323415f0dSnerun $username = is_array($user) ? ($user['user'] ?? '') : $user; 14423415f0dSnerun $userinfo = $auth->getUserData($username); 14523415f0dSnerun 14685db55c8Snerun if (!$userinfo) return null; 14785db55c8Snerun if (empty($title) && !empty($userinfo['name'])) $title = hsc($userinfo['name']); 14823415f0dSnerun 1492f9fb8e6Snerun $ns = $this->resolveTokens($this->getConf('namespace')); 15085db55c8Snerun $existingFiles = []; 15185db55c8Snerun 15285db55c8Snerun // Scan all allowed formats. 15323415f0dSnerun foreach (self::ALLOWED_FORMATS as $format) { 15423415f0dSnerun $imagePath = $ns . ':' . $username . $format; 15523415f0dSnerun $imageFile = mediaFN($imagePath); 15623415f0dSnerun 15723415f0dSnerun if (file_exists($imageFile)) { 15885db55c8Snerun $existingFiles[$imagePath] = filesize($imageFile); 15923415f0dSnerun } 16023415f0dSnerun } 16123415f0dSnerun 16285db55c8Snerun if ($existingFiles) { 16385db55c8Snerun // Returns the file with the smallest size. 16485db55c8Snerun asort($existingFiles); 16585db55c8Snerun $bestPath = key($existingFiles); 16685db55c8Snerun return ml($bestPath, ['w' => $size, 'h' => $size], true, '&', false); 167f443324cSnerun } 16885db55c8Snerun 16985db55c8Snerun // No local files found → generate MonsterID if it's the selected 17085db55c8Snerun // fallback 17185db55c8Snerun if ($this->getConf('local_default') === 'monsterid') { 17285db55c8Snerun if ($this->saveMonsterIdAvatar($username, 120)) { 17385db55c8Snerun $monsterPath = $ns . ':' . $username . '.png'; 17485db55c8Snerun if (file_exists(mediaFN($monsterPath))) { 17585db55c8Snerun return ml($monsterPath, ['w' => $size, 'h' => $size], true, '&', false); 176f443324cSnerun } 177f443324cSnerun } 1784c5c7bddSnerun } 179f443324cSnerun 18023415f0dSnerun return null; 18123415f0dSnerun } 18223415f0dSnerun 183f443324cSnerun private function saveMonsterIdAvatar(string $username, int $size): bool 184f443324cSnerun { 1852f9fb8e6Snerun global $INFO; 1862f9fb8e6Snerun 1872f9fb8e6Snerun $currentUser = cleanID($INFO['client'] ?? ''); 1882f9fb8e6Snerun $username = cleanID($username); 1892f9fb8e6Snerun 1902f9fb8e6Snerun // It only allows you to save your own avatar. 1912f9fb8e6Snerun if ($username !== $currentUser) { 1922f9fb8e6Snerun return false; 1932f9fb8e6Snerun } 1942f9fb8e6Snerun 1952f9fb8e6Snerun $ns = $this->resolveTokens($this->getConf('namespace')); 196f443324cSnerun $filename = $ns . ':' . $username . '.png'; 197f443324cSnerun $filepath = mediaFN($filename); 198f443324cSnerun 19985db55c8Snerun // If any local avatar of the user already exists, it will not generate 20085db55c8Snerun // a MonsterID. 20185db55c8Snerun foreach (self::ALLOWED_FORMATS as $format) { 20285db55c8Snerun if (file_exists(mediaFN($ns . ':' . $username . $format))) { 203f2a729a9Snerun return true; 204f2a729a9Snerun } 20585db55c8Snerun } 206f2a729a9Snerun 20785db55c8Snerun // MonsterID URL for the user 208f443324cSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower($username)); 209f443324cSnerun $monsterUrl = DOKU_URL . 'lib/plugins/avatar/monsterid.php?seed=' . $seed . '&size=' . $size; 210f443324cSnerun 211f443324cSnerun // Download the image using file_get_contents 212f443324cSnerun $imageData = @file_get_contents($monsterUrl); 21385db55c8Snerun if ($imageData === false) return false; 214f443324cSnerun 215f443324cSnerun // creates the directory if it does not exist 216f443324cSnerun io_makeFileDir($filepath); 217f443324cSnerun 218f443324cSnerun // Save the image 219f443324cSnerun return file_put_contents($filepath, $imageData) !== false; 220f443324cSnerun } 221f443324cSnerun 22223415f0dSnerun private function getGravatarUrl(string $mail, int $size): string 22323415f0dSnerun { 22423415f0dSnerun $seed = md5(dokuwiki\Utf8\PhpString::strtolower($mail)); 22523415f0dSnerun 22623415f0dSnerun $default = function_exists('imagecreatetruecolor') 22723415f0dSnerun ? $this->getMonsterIdUrl($seed, $size) 22823415f0dSnerun : $this->getDefaultImageUrl($size); 22923415f0dSnerun 23023415f0dSnerun if (!mail_isvalid($mail)) { 23123415f0dSnerun return $default; 23223415f0dSnerun } 23323415f0dSnerun 23423415f0dSnerun $params = [ 23523415f0dSnerun 's' => $size, 23623415f0dSnerun 'r' => $this->getConf('rating') 23723415f0dSnerun ]; 23823415f0dSnerun 239*f748238fSnerun $gravatar_default = $this->getConf('gravatar_default'); 240*f748238fSnerun 241*f748238fSnerun if ($gravatar_default !== 'default') { 242*f748238fSnerun $params['d'] = $gravatar_default; 243*f748238fSnerun } 244*f748238fSnerun 24554eb7e18Snerun return self::GRAVATAR_BASE . $seed . '?' . http_build_query($params); 24623415f0dSnerun } 24723415f0dSnerun 24823415f0dSnerun private function getMonsterIdUrl(string $seed, int $size): string 24923415f0dSnerun { 250f2a729a9Snerun $file = 'monsterid.php?seed=' . $seed . '&size=' . $size; 25123415f0dSnerun return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 25223415f0dSnerun } 25323415f0dSnerun 25423415f0dSnerun private function getDefaultImageUrl(int $size): string 25523415f0dSnerun { 2564cc391acSnerun $file = 'images/default_' . $size . '.png'; 25723415f0dSnerun return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true); 25823415f0dSnerun } 25923415f0dSnerun} 260