xref: /plugin/avatar/helper.php (revision 4cc391ac553898c28ce9655d9e18ed9e95688203)
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