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