1<?php
2/**
3 * Avatar Plugin for DokuWiki
4 *
5 * Displays avatar images with syntax {{avatar>email@domain.com}}
6 * Supports local avatars, Gravatar.com, and MonsterID fallback
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Esther Brunner <wikidesign@gmail.com>
10 * @author     Daniel Dias Rodrigues <danieldiasr@gmail.com> (modernization)
11 */
12
13declare(strict_types=1);
14
15if (!defined('DOKU_INC')) die();
16
17class helper_plugin_avatar extends DokuWiki_Plugin
18{
19    private const DEFAULT_SIZES = [
20        'small'  => 20,
21        'medium' => 40,
22        'large'  => 80,
23        'xlarge' => 120
24    ];
25
26    private const ALLOWED_FORMATS = ['.png', '.jpg', '.gif'];
27    private const GRAVATAR_BASE = 'https://secure.gravatar.com/avatar/';
28
29    private array $avatarCache = [];
30
31    public function getMethods(): array
32    {
33        return [
34            [
35                'name'   => 'renderXhtml',
36                'desc'   => 'Returns the XHTML to display an avatar',
37                'params' => [
38                    'user'  => 'string|array',
39                    'title' => 'string',
40                    'align' => 'string',
41                    'size'  => 'int'
42                ],
43                'return' => ['xhtml' => 'string']
44            ]
45        ];
46    }
47
48    /**
49     * Renders the avatar as XHTML <img>
50     */
51    public function renderXhtml(string|array $user, string $title = '', ?string $align = '', ?int $size = null): string
52    {
53        $src = $this->resolveAvatarUrl($user, $title, $size);
54
55        return '<img src="' . $src . '" ' .
56               'class="media' . $align . ' photo fn" ' .
57               'title="' . $title . '" ' .
58               'alt="' . $title . '" ' .
59               'width="' . (string) $size . '" ' .
60               'height="' . (string) $size . '" />';
61    }
62
63    /**
64     * Gets or generates the avatar URL for a user/email
65     */
66    public function resolveAvatarUrl(string|array $user, ?string &$title = null, ?int &$size = null): string
67    {
68        global $auth;
69
70        $size = $this->normalizeSize($size);
71        $cacheKey = $this->getCacheKey($user, $title, $size);
72
73        if (isset($this->avatarCache[$cacheKey])) {
74            return $this->avatarCache[$cacheKey];
75        }
76
77        $mail = $this->extractUserData($user, $title);
78        $src = $this->tryLocalAvatar($user, $title, $size);
79
80        if (!$src) {
81            $src = $this->getGravatarUrl($mail, $size);
82        }
83
84        if (empty($title)) {
85            $title = obfuscate($mail);
86        }
87
88        $this->avatarCache[$cacheKey] = $src;
89        return $src;
90    }
91
92    private function normalizeSize(?int $size): int
93    {
94        if ($size && $size > 0) {
95            return $size;
96        }
97
98        $confSize = (int) $this->getConf('size');
99        return $confSize > 0 ? $confSize : self::DEFAULT_SIZES['medium'];
100    }
101
102    private function getCacheKey(string|array $user, ?string $title, int $size): string
103    {
104        $userKey = is_array($user) ? ($user['mail'] ?? '') : $user;
105        return md5($userKey . $title . $size);
106    }
107
108    private function extractUserData(string|array $user, ?string &$title): string
109    {
110        if (is_array($user)) {
111            if (empty($title) && !empty($user['name'])) {
112                $title = hsc($user['name']);
113            }
114            return $user['mail'] ?? '';
115        }
116        return $user;
117    }
118
119    private function tryLocalAvatar(string|array $user, ?string &$title, int $size): ?string
120    {
121        global $auth;
122
123        $username = is_array($user) ? ($user['user'] ?? '') : $user;
124        $userinfo = $auth->getUserData($username);
125
126        if (!$userinfo) {
127            return null;
128        }
129
130        if (empty($title) && !empty($userinfo['name'])) {
131            $title = hsc($userinfo['name']);
132        }
133
134        $ns = $this->getConf('namespace');
135        foreach (self::ALLOWED_FORMATS as $format) {
136            $imagePath = $ns . ':' . $username . $format;
137            $imageFile = mediaFN($imagePath);
138
139            if (file_exists($imageFile)) {
140                return ml($imagePath, ['w' => $size, 'h' => $size], true, '&', false);
141            }
142        }
143
144        // If it is the user itself, it generates and saves Monsterid
145        if (is_string($user) && $user === $username) {
146            if ($this->saveMonsterIdAvatar($username, 120)) { // Save large size to quality
147                $imagePath = $ns . ':' . $username . '.png';
148                if (file_exists(mediaFN($imagePath))) {
149                    return ml($imagePath, ['w' => $size, 'h' => $size], true, '&', false);
150                }
151            }
152        }
153
154        return null;
155    }
156
157    private function saveMonsterIdAvatar(string $username, int $size): bool
158    {
159        $ns = $this->getConf('namespace');
160        $filename = $ns . ':' . $username . '.png';
161        $filepath = mediaFN($filename);
162
163        // Monsterid URL for the user
164        $seed = md5(dokuwiki\Utf8\PhpString::strtolower($username));
165        $monsterUrl = DOKU_URL . 'lib/plugins/avatar/monsterid.php?seed=' . $seed . '&size=' . $size;
166
167        // Download the image using file_get_contents
168        $imageData = @file_get_contents($monsterUrl);
169        if ($imageData === false) {
170            return false;
171        }
172
173        // creates the directory if it does not exist
174        io_makeFileDir($filepath);
175
176        // Save the image
177        return file_put_contents($filepath, $imageData) !== false;
178    }
179
180    private function getGravatarUrl(string $mail, int $size): string
181    {
182        $seed = md5(dokuwiki\Utf8\PhpString::strtolower($mail));
183
184        $default = function_exists('imagecreatetruecolor')
185            ? $this->getMonsterIdUrl($seed, $size)
186            : $this->getDefaultImageUrl($size);
187
188        if (!mail_isvalid($mail)) {
189            return $default;
190        }
191
192        $params = [
193            's' => $size,
194            'd' => $this->getConf('default'),
195            'r' => $this->getConf('rating')
196        ];
197
198        return self::GRAVATAR_BASE . $seed . '?' . http_build_query($params);
199    }
200
201    private function getMonsterIdUrl(string $seed, int $size): string
202    {
203        $file = 'monsterid.php?seed=' . $seed . '&size=' . $size . '&.png';
204        return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true);
205    }
206
207    private function getDefaultImageUrl(int $size): string
208    {
209        $validSizes = array_values(self::DEFAULT_SIZES);
210        $realSize = in_array($size, $validSizes, true) ? $size : self::DEFAULT_SIZES['xlarge'];
211        $file = 'images/default_' . $realSize . '.png';
212        return ml(DOKU_URL . 'lib/plugins/avatar/' . $file, 'cache=recache', true, '&', true);
213    }
214}
215
216