xref: /plugin/pagesicon/helper.php (revision b603bbe1e83f2122ab63b4df278133cf619e6be8)
1da933f89SLORTET<?php
2da933f89SLORTETif (!defined('DOKU_INC')) die();
3da933f89SLORTET
4da933f89SLORTETclass helper_plugin_pagesicon extends DokuWiki_Plugin
5da933f89SLORTET{
6*b603bbe1SLORTET    private const BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH = 'lib/plugins/pagesicon/images/default_image.png';
7*b603bbe1SLORTET
8*b603bbe1SLORTET    private function getBundledDefaultImagePath(): string {
9*b603bbe1SLORTET        return DOKU_INC . self::BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH;
1074a9e763SLORTET    }
1174a9e763SLORTET
12*b603bbe1SLORTET    private function getBundledDefaultImageUrl(): string {
1374a9e763SLORTET        $path = $this->getBundledDefaultImagePath();
1474a9e763SLORTET        if (!@file_exists($path)) return '';
1574a9e763SLORTET
1674a9e763SLORTET        $base = rtrim((string)DOKU_BASE, '/');
17*b603bbe1SLORTET        $url = $base . '/' . self::BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH;
1874a9e763SLORTET        $mtime = @filemtime($path);
1974a9e763SLORTET        return $this->appendVersionToUrl($url, $mtime ? (int)$mtime : 0);
2074a9e763SLORTET    }
2174a9e763SLORTET
22*b603bbe1SLORTET    private function getConfiguredDefaultImageMediaID() {
2374a9e763SLORTET        $mediaID = cleanID((string)$this->getConf('default_image'));
2474a9e763SLORTET        if ($mediaID === '') return false;
2574a9e763SLORTET        if (!@file_exists(mediaFN($mediaID))) return false;
2674a9e763SLORTET        return $mediaID;
2774a9e763SLORTET    }
2874a9e763SLORTET
29*b603bbe1SLORTET    private function getMediaMTime(string $mediaID): int {
30da933f89SLORTET        $mediaID = cleanID($mediaID);
31da933f89SLORTET        if ($mediaID === '') return 0;
32da933f89SLORTET        $file = mediaFN($mediaID);
33da933f89SLORTET        if (!@file_exists($file)) return 0;
34da933f89SLORTET        $mtime = @filemtime($file);
35da933f89SLORTET        return $mtime ? (int)$mtime : 0;
36da933f89SLORTET    }
37da933f89SLORTET
38*b603bbe1SLORTET    private function appendVersionToUrl(string $url, int $mtime): string {
39da933f89SLORTET        if ($url === '' || $mtime <= 0) return $url;
40da933f89SLORTET        $sep = strpos($url, '?') === false ? '?' : '&';
41da933f89SLORTET        return $url . $sep . 'pi_ts=' . $mtime;
42da933f89SLORTET    }
43da933f89SLORTET
44*b603bbe1SLORTET    /**
45*b603bbe1SLORTET     * Added in version 2026-03-06.
46*b603bbe1SLORTET     * Notifies consumers that an icon changed and triggers cache invalidation hooks.
47*b603bbe1SLORTET     */
48*b603bbe1SLORTET    public function notifyIconUpdated(string $targetPage, string $action = 'update', string $mediaID = ''): void {
49da933f89SLORTET        global $conf;
50da933f89SLORTET
51da933f89SLORTET        @io_saveFile($conf['cachedir'] . '/purgefile', time());
52da933f89SLORTET
53da933f89SLORTET        $data = [
54da933f89SLORTET            'target_page' => cleanID($targetPage),
55da933f89SLORTET            'action' => $action,
56da933f89SLORTET            'media_id' => cleanID($mediaID),
57da933f89SLORTET        ];
58da933f89SLORTET        \dokuwiki\Extension\Event::createAndTrigger('PLUGIN_PAGESICON_UPDATED', $data);
59da933f89SLORTET    }
60da933f89SLORTET
61*b603bbe1SLORTET    /**
62*b603bbe1SLORTET     * Added in version 2026-03-11.
63*b603bbe1SLORTET     * Returns the configured filename templates for the requested icon variant.
64*b603bbe1SLORTET     */
65*b603bbe1SLORTET    public function getVariantTemplates(string $variant): array {
66*b603bbe1SLORTET        $confKey = $variant === 'small' ? 'icon_thumbnail_name' : 'icon_name';
67*b603bbe1SLORTET        $raw = (string)$this->getConf($confKey);
68*b603bbe1SLORTET
69*b603bbe1SLORTET        if (trim($raw) === '') {
70*b603bbe1SLORTET            trigger_error('pagesicon: missing required configuration "' . $confKey . '"', E_USER_WARNING);
71*b603bbe1SLORTET            return [];
72*b603bbe1SLORTET        }
73*b603bbe1SLORTET
74*b603bbe1SLORTET        $templates = array_values(array_unique(array_filter(array_map('trim', explode(';', $raw)))));
75*b603bbe1SLORTET        if (!$templates) {
76*b603bbe1SLORTET            trigger_error('pagesicon: configuration "' . $confKey . '" does not contain any usable value', E_USER_WARNING);
77*b603bbe1SLORTET        }
78*b603bbe1SLORTET
79*b603bbe1SLORTET        return $templates;
80*b603bbe1SLORTET    }
81*b603bbe1SLORTET
82*b603bbe1SLORTET    /**
83*b603bbe1SLORTET     * Added in version 2026-03-11.
84*b603bbe1SLORTET     * Normalizes an icon filename candidate to its base media name without namespace or extension.
85*b603bbe1SLORTET     */
86*b603bbe1SLORTET    public function normalizeIconBaseName(string $name): string {
87*b603bbe1SLORTET        $name = trim($name);
88*b603bbe1SLORTET        if ($name === '') return '';
89*b603bbe1SLORTET        $name = noNS($name);
90*b603bbe1SLORTET        $name = preg_replace('/\.[a-z0-9]+$/i', '', $name);
91*b603bbe1SLORTET        $name = cleanID($name);
92*b603bbe1SLORTET        return str_replace(':', '', $name);
93*b603bbe1SLORTET    }
94*b603bbe1SLORTET
95*b603bbe1SLORTET    /**
96*b603bbe1SLORTET     * Added in version 2026-03-11.
97*b603bbe1SLORTET     * Returns the allowed target base names for an upload, indexed by their normalized value.
98*b603bbe1SLORTET     */
99*b603bbe1SLORTET    public function getUploadNameChoices(string $targetPage, string $variant): array {
100*b603bbe1SLORTET        $pageID = noNS($targetPage);
101*b603bbe1SLORTET        $choices = [];
102*b603bbe1SLORTET
103*b603bbe1SLORTET        foreach ($this->getVariantTemplates($variant) as $tpl) {
104*b603bbe1SLORTET            $resolved = str_replace('~pagename~', $pageID, $tpl);
105*b603bbe1SLORTET            $base = $this->normalizeIconBaseName($resolved);
106*b603bbe1SLORTET            if ($base === '') continue;
107*b603bbe1SLORTET            $choices[$base] = $base . '.ext';
108*b603bbe1SLORTET        }
109*b603bbe1SLORTET
110*b603bbe1SLORTET        return $choices;
111*b603bbe1SLORTET    }
112*b603bbe1SLORTET
113*b603bbe1SLORTET    private function buildConfiguredCandidatesFromRaw(string $raw, string $namespace, string $pageID): array {
114da933f89SLORTET        $configured = [];
115da933f89SLORTET        $entries = array_filter(array_map('trim', explode(';', $raw)));
116da933f89SLORTET
117da933f89SLORTET        foreach ($entries as $entry) {
118da933f89SLORTET            $name = str_replace('~pagename~', $pageID, $entry);
119da933f89SLORTET            if ($name === '') continue;
120da933f89SLORTET
121da933f89SLORTET            if (strpos($name, ':') === false && $namespace !== '') {
122da933f89SLORTET                $configured[] = $namespace . ':' . $name;
123da933f89SLORTET            } else {
124da933f89SLORTET                $configured[] = ltrim($name, ':');
125da933f89SLORTET            }
126da933f89SLORTET        }
127da933f89SLORTET
128da933f89SLORTET        return array_values(array_unique($configured));
129da933f89SLORTET    }
130da933f89SLORTET
131*b603bbe1SLORTET    private function buildConfiguredCandidates(string $namespace, string $pageID, string $sizeMode): array {
132da933f89SLORTET        $bigRaw = trim((string)$this->getConf('icon_name'));
133da933f89SLORTET        $smallRaw = trim((string)$this->getConf('icon_thumbnail_name'));
134da933f89SLORTET
135da933f89SLORTET        $big = $this->buildConfiguredCandidatesFromRaw($bigRaw, $namespace, $pageID);
136da933f89SLORTET        $small = $this->buildConfiguredCandidatesFromRaw($smallRaw, $namespace, $pageID);
137da933f89SLORTET
138da933f89SLORTET        if ($sizeMode === 'big') return $big;
139da933f89SLORTET        if ($sizeMode === 'small') return $small;
140da933f89SLORTET        if ($sizeMode === 'smallorbig') return array_values(array_unique(array_merge($small, $big)));
141da933f89SLORTET
142da933f89SLORTET        // Default: bigorsmall
143da933f89SLORTET        return array_values(array_unique(array_merge($big, $small)));
144da933f89SLORTET    }
145da933f89SLORTET
146*b603bbe1SLORTET    private function normalizeSizeMode(string $size): string {
147da933f89SLORTET        $size = strtolower(trim($size));
148da933f89SLORTET        $allowed = ['big', 'small', 'bigorsmall', 'smallorbig'];
149da933f89SLORTET        if (in_array($size, $allowed, true)) return $size;
150da933f89SLORTET        return 'bigorsmall';
151da933f89SLORTET    }
152da933f89SLORTET
153*b603bbe1SLORTET    /**
154*b603bbe1SLORTET     * Added in version 2026-03-11.
155*b603bbe1SLORTET     * Returns the configured list of allowed icon file extensions.
156*b603bbe1SLORTET     */
157*b603bbe1SLORTET    public function getConfiguredExtensions(): array {
158da933f89SLORTET        $raw = trim((string)$this->getConf('extensions'));
159*b603bbe1SLORTET        if ($raw === '') {
160*b603bbe1SLORTET            trigger_error('pagesicon: missing required configuration "extensions"', E_USER_WARNING);
161*b603bbe1SLORTET            return [];
162*b603bbe1SLORTET        }
163da933f89SLORTET
164da933f89SLORTET        $extensions = array_values(array_unique(array_filter(array_map(function ($ext) {
165da933f89SLORTET            return strtolower(ltrim(trim((string)$ext), '.'));
166da933f89SLORTET        }, explode(';', $raw)))));
167da933f89SLORTET
168*b603bbe1SLORTET        if (!$extensions) {
169*b603bbe1SLORTET            trigger_error('pagesicon: configuration "extensions" does not contain any usable value', E_USER_WARNING);
170da933f89SLORTET        }
171da933f89SLORTET
172*b603bbe1SLORTET        return $extensions;
173*b603bbe1SLORTET    }
174*b603bbe1SLORTET
175*b603bbe1SLORTET    private function hasKnownExtension(string $name, array $extensions): bool {
176da933f89SLORTET        $fileExt = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
177da933f89SLORTET        return $fileExt !== '' && in_array($fileExt, $extensions, true);
178da933f89SLORTET    }
179da933f89SLORTET
180*b603bbe1SLORTET    /**
181*b603bbe1SLORTET     * Added in version 2026-03-09.
182*b603bbe1SLORTET     * Resolves the icon media ID for a page, or false when no icon matches.
183*b603bbe1SLORTET     * Replaces the older getPageImage() name.
184*b603bbe1SLORTET     */
18574a9e763SLORTET    public function getPageIconId(
18674a9e763SLORTET        string $namespace,
18774a9e763SLORTET        string $pageID,
18874a9e763SLORTET        string $size = 'bigorsmall'
18974a9e763SLORTET    )
190da933f89SLORTET    {
191da933f89SLORTET        $sizeMode = $this->normalizeSizeMode($size);
192*b603bbe1SLORTET        $extensions = $this->getConfiguredExtensions();
193da933f89SLORTET        $namespace = $namespace ?: '';
194da933f89SLORTET        $pageBase = $namespace ? ($namespace . ':' . $pageID) : $pageID;
195da933f89SLORTET        $nsBase = $namespace ? ($namespace . ':') : '';
196da933f89SLORTET
197da933f89SLORTET        $genericBig = [
198da933f89SLORTET            $pageBase,
199da933f89SLORTET            $pageBase . ':logo',
200da933f89SLORTET            $nsBase . 'logo',
201da933f89SLORTET        ];
202da933f89SLORTET        $genericSmall = [
203da933f89SLORTET            $pageBase . ':thumbnail',
204da933f89SLORTET            $nsBase . 'thumbnail',
205da933f89SLORTET        ];
206da933f89SLORTET
207da933f89SLORTET        if ($sizeMode === 'big') {
208da933f89SLORTET            $generic = $genericBig;
209da933f89SLORTET        } elseif ($sizeMode === 'small') {
210da933f89SLORTET            $generic = $genericSmall;
211da933f89SLORTET        } elseif ($sizeMode === 'smallorbig') {
212da933f89SLORTET            $generic = array_merge($genericSmall, $genericBig);
213da933f89SLORTET        } else {
214da933f89SLORTET            $generic = array_merge($genericBig, $genericSmall);
215da933f89SLORTET        }
216da933f89SLORTET
217da933f89SLORTET        $imageNames = array_merge($this->buildConfiguredCandidates($namespace, $pageID, $sizeMode), $generic);
218da933f89SLORTET
219da933f89SLORTET        foreach ($imageNames as $name) {
220da933f89SLORTET            if ($this->hasKnownExtension($name, $extensions)) {
221da933f89SLORTET                if (@file_exists(mediaFN($name))) return $name;
222da933f89SLORTET                continue;
223da933f89SLORTET            }
224da933f89SLORTET
225da933f89SLORTET            foreach ($extensions as $ext) {
226da933f89SLORTET                $path = $name . '.' . $ext;
227da933f89SLORTET                if (@file_exists(mediaFN($path))) return $path;
228da933f89SLORTET            }
229da933f89SLORTET        }
230da933f89SLORTET
231da933f89SLORTET        return false;
232da933f89SLORTET    }
233da933f89SLORTET
234*b603bbe1SLORTET    /**
235*b603bbe1SLORTET     * Added in version 2026-03-06.
236*b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
237*b603bbe1SLORTET     * Use getPageIconId() instead.
238*b603bbe1SLORTET     */
23974a9e763SLORTET    public function getPageImage(
24074a9e763SLORTET        string $namespace,
24174a9e763SLORTET        string $pageID,
24274a9e763SLORTET        string $size = 'bigorsmall',
24374a9e763SLORTET        bool $withDefault = false
24474a9e763SLORTET    ) {
24574a9e763SLORTET        return $this->getPageIconId($namespace, $pageID, $size);
24674a9e763SLORTET    }
24774a9e763SLORTET
248*b603bbe1SLORTET    /**
249*b603bbe1SLORTET     * Added in version 2026-03-06.
250*b603bbe1SLORTET     * Returns the icon management URL for a page, or null when upload is not allowed.
251*b603bbe1SLORTET     */
252*b603bbe1SLORTET    public function getUploadIconPage(string $targetPage = '') {
253da933f89SLORTET        global $ID;
254da933f89SLORTET
255da933f89SLORTET        $targetPage = cleanID($targetPage);
256da933f89SLORTET        if ($targetPage === '') {
257da933f89SLORTET            $targetPage = cleanID(getNS((string)$ID));
258da933f89SLORTET        }
259da933f89SLORTET        if ($targetPage === '') {
260da933f89SLORTET            $targetPage = cleanID((string)$ID);
261da933f89SLORTET        }
262da933f89SLORTET        if ($targetPage === '') return null;
263da933f89SLORTET
264da933f89SLORTET        if (auth_quickaclcheck($targetPage) < AUTH_UPLOAD) {
265da933f89SLORTET            return null;
266da933f89SLORTET        }
267da933f89SLORTET
268da933f89SLORTET        return wl($targetPage, ['do' => 'pagesicon']);
269da933f89SLORTET    }
270da933f89SLORTET
271*b603bbe1SLORTET    /**
272*b603bbe1SLORTET     * Added in version 2026-03-09.
273*b603bbe1SLORTET     * Resolves the icon media ID associated with a media file, or false when none matches.
274*b603bbe1SLORTET     * Replaces the older getMediaImage() name.
275*b603bbe1SLORTET     */
276*b603bbe1SLORTET    public function getMediaIconId(string $mediaID, string $size = 'bigorsmall') {
277da933f89SLORTET        $mediaID = cleanID($mediaID);
278da933f89SLORTET        if ($mediaID === '') return false;
279da933f89SLORTET
280da933f89SLORTET        $namespace = getNS($mediaID);
281da933f89SLORTET        $filename = noNS($mediaID);
282da933f89SLORTET        $base = (string)pathinfo($filename, PATHINFO_FILENAME);
283da933f89SLORTET        $pageID = cleanID($base);
284da933f89SLORTET        if ($pageID === '') return false;
285da933f89SLORTET
28674a9e763SLORTET        return $this->getPageIconId($namespace, $pageID, $size);
287da933f89SLORTET    }
288da933f89SLORTET
289*b603bbe1SLORTET    /**
290*b603bbe1SLORTET     * Added in version 2026-03-06.
291*b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
292*b603bbe1SLORTET     * Use getMediaIconId() instead.
293*b603bbe1SLORTET     */
294*b603bbe1SLORTET    public function getMediaImage(string $mediaID, string $size = 'bigorsmall', bool $withDefault = false) {
29574a9e763SLORTET        return $this->getMediaIconId($mediaID, $size);
29674a9e763SLORTET    }
29774a9e763SLORTET
298*b603bbe1SLORTET    private function matchesPageIconVariant(string $mediaID, string $namespace, string $pageID): bool {
299*b603bbe1SLORTET        $bigIconID = $this->getPageIconId($namespace, $pageID, 'big');
300*b603bbe1SLORTET        if ($bigIconID && cleanID((string)$bigIconID) === $mediaID) return true;
301*b603bbe1SLORTET
302*b603bbe1SLORTET        $smallIconID = $this->getPageIconId($namespace, $pageID, 'small');
303*b603bbe1SLORTET        if ($smallIconID && cleanID((string)$smallIconID) === $mediaID) return true;
304*b603bbe1SLORTET
305*b603bbe1SLORTET        return false;
306*b603bbe1SLORTET    }
307*b603bbe1SLORTET
308*b603bbe1SLORTET    /**
309*b603bbe1SLORTET     * Added in version 2026-03-11.
310*b603bbe1SLORTET     * Checks whether a media ID should be considered a page icon managed by the pagesicon plugin.
311*b603bbe1SLORTET     */
312*b603bbe1SLORTET    public function isPageIconMedia(string $mediaID): bool {
313*b603bbe1SLORTET        global $conf;
314*b603bbe1SLORTET
315*b603bbe1SLORTET        $mediaID = cleanID($mediaID);
316*b603bbe1SLORTET        if ($mediaID === '') return false;
317*b603bbe1SLORTET
318*b603bbe1SLORTET        $namespace = getNS($mediaID);
319*b603bbe1SLORTET        $filename = noNS($mediaID);
320*b603bbe1SLORTET        $basename = cleanID((string)pathinfo($filename, PATHINFO_FILENAME));
321*b603bbe1SLORTET        if ($basename === '') return false;
322*b603bbe1SLORTET
323*b603bbe1SLORTET        // Case 1: this media is the big or small icon selected for a page with the same base name.
324*b603bbe1SLORTET        $sameNamePageID = $namespace !== '' ? ($namespace . ':' . $basename) : $basename;
325*b603bbe1SLORTET        if (page_exists($sameNamePageID)) {
326*b603bbe1SLORTET            if ($this->matchesPageIconVariant($mediaID, $namespace, $basename)) return true;
327*b603bbe1SLORTET        }
328*b603bbe1SLORTET
329*b603bbe1SLORTET        // Case 2: this media is the big or small icon selected for a page whose ID matches the namespace.
330*b603bbe1SLORTET        if ($namespace !== '' && page_exists($namespace)) {
331*b603bbe1SLORTET            $parentNamespace = getNS($namespace);
332*b603bbe1SLORTET            $pageID = noNS($namespace);
333*b603bbe1SLORTET            if ($this->matchesPageIconVariant($mediaID, $parentNamespace, $pageID)) return true;
334*b603bbe1SLORTET        }
335*b603bbe1SLORTET
336*b603bbe1SLORTET        // Case 3: this media is the big or small icon selected for a page whose ID
337*b603bbe1SLORTET        // matches the namespace leaf, for example "...:playground:playground".
338*b603bbe1SLORTET        if ($namespace !== '') {
339*b603bbe1SLORTET            $namespaceLeaf = noNS($namespace);
340*b603bbe1SLORTET            $leafPageID = cleanID($namespace . ':' . $namespaceLeaf);
341*b603bbe1SLORTET            if ($leafPageID !== '' && page_exists($leafPageID)) {
342*b603bbe1SLORTET                if ($this->matchesPageIconVariant($mediaID, $namespace, $namespaceLeaf)) return true;
343*b603bbe1SLORTET            }
344*b603bbe1SLORTET        }
345*b603bbe1SLORTET
346*b603bbe1SLORTET        // Case 4: this media is the big or small icon selected for the namespace start page
347*b603bbe1SLORTET        // (for example "...:start"), which often carries the visible page content.
348*b603bbe1SLORTET        if ($namespace !== '' && isset($conf['start'])) {
349*b603bbe1SLORTET            $startId = cleanID((string)$conf['start']);
350*b603bbe1SLORTET            $startPage = $startId !== '' ? cleanID($namespace . ':' . $startId) : '';
351*b603bbe1SLORTET            if ($startPage !== '' && page_exists($startPage)) {
352*b603bbe1SLORTET                if ($this->matchesPageIconVariant($mediaID, $namespace, noNS($startPage))) return true;
353*b603bbe1SLORTET            }
354*b603bbe1SLORTET        }
355*b603bbe1SLORTET
356*b603bbe1SLORTET        return false;
357*b603bbe1SLORTET    }
358*b603bbe1SLORTET
359*b603bbe1SLORTET    /**
360*b603bbe1SLORTET     * Added in version 2026-03-09.
361*b603bbe1SLORTET     * Returns the configured default icon URL, or the bundled fallback image when available.
362*b603bbe1SLORTET     */
363*b603bbe1SLORTET    public function getDefaultIconUrl(array $params = ['width' => 55], ?int &$mtime = null) {
36474a9e763SLORTET        $mediaID = $this->getConfiguredDefaultImageMediaID();
36574a9e763SLORTET        if ($mediaID) {
36674a9e763SLORTET            $mtime = $this->getMediaMTime((string)$mediaID);
36774a9e763SLORTET            $url = (string)ml((string)$mediaID, $params);
36874a9e763SLORTET            if ($url === '') return false;
36974a9e763SLORTET            return $this->appendVersionToUrl($url, $mtime);
37074a9e763SLORTET        }
37174a9e763SLORTET
37274a9e763SLORTET        $mtime = 0;
37374a9e763SLORTET        $bundled = $this->getBundledDefaultImageUrl();
37474a9e763SLORTET        if ($bundled !== '') return $bundled;
37574a9e763SLORTET
37674a9e763SLORTET        return false;
37774a9e763SLORTET    }
37874a9e763SLORTET
379*b603bbe1SLORTET    /**
380*b603bbe1SLORTET     * Added in version 2026-03-09.
381*b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
382*b603bbe1SLORTET     * Use getDefaultIconUrl() instead.
383*b603bbe1SLORTET     */
384*b603bbe1SLORTET    public function getDefaultImageIcon(array $params = ['width' => 55], ?int &$mtime = null) {
38574a9e763SLORTET        return $this->getDefaultIconUrl($params, $mtime);
38674a9e763SLORTET    }
38774a9e763SLORTET
388*b603bbe1SLORTET    /**
389*b603bbe1SLORTET     * Added in version 2026-03-09.
390*b603bbe1SLORTET     * Returns a versioned icon URL for a page, or false when no icon matches.
391*b603bbe1SLORTET     * Replaces the older getImageIcon() name.
392*b603bbe1SLORTET     */
39374a9e763SLORTET    public function getPageIconUrl(
394da933f89SLORTET        string $namespace,
395da933f89SLORTET        string $pageID,
396da933f89SLORTET        string $size = 'bigorsmall',
397da933f89SLORTET        array $params = ['width' => 55],
39874a9e763SLORTET        ?int &$mtime = null,
39974a9e763SLORTET        bool $withDefault = false
400da933f89SLORTET    ) {
40174a9e763SLORTET        $mediaID = $this->getPageIconId($namespace, $pageID, $size);
402da933f89SLORTET        if (!$mediaID) {
40374a9e763SLORTET            if ($withDefault) {
40474a9e763SLORTET                return $this->getDefaultIconUrl($params, $mtime);
40574a9e763SLORTET            }
406da933f89SLORTET            $mtime = 0;
407da933f89SLORTET            return false;
408da933f89SLORTET        }
409da933f89SLORTET
410da933f89SLORTET        $mtime = $this->getMediaMTime((string)$mediaID);
411da933f89SLORTET        $url = (string)ml((string)$mediaID, $params);
412da933f89SLORTET        if ($url === '') return false;
413da933f89SLORTET        return $this->appendVersionToUrl($url, $mtime);
414da933f89SLORTET    }
415da933f89SLORTET
416*b603bbe1SLORTET    /**
417*b603bbe1SLORTET     * Added in version 2026-03-06.
418*b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
419*b603bbe1SLORTET     * Use getPageIconUrl() instead.
420*b603bbe1SLORTET     */
42174a9e763SLORTET    public function getImageIcon(
42274a9e763SLORTET        string $namespace,
42374a9e763SLORTET        string $pageID,
42474a9e763SLORTET        string $size = 'bigorsmall',
42574a9e763SLORTET        array $params = ['width' => 55],
42674a9e763SLORTET        ?int &$mtime = null,
42774a9e763SLORTET        bool $withDefault = false
42874a9e763SLORTET    ) {
42974a9e763SLORTET        return $this->getPageIconUrl($namespace, $pageID, $size, $params, $mtime, $withDefault);
43074a9e763SLORTET    }
43174a9e763SLORTET
432*b603bbe1SLORTET    /**
433*b603bbe1SLORTET     * Added in version 2026-03-09.
434*b603bbe1SLORTET     * Returns a versioned icon URL for a media file, or false when no icon matches.
435*b603bbe1SLORTET     * Replaces the older getMediaIcon() name.
436*b603bbe1SLORTET     */
43774a9e763SLORTET    public function getMediaIconUrl(
438da933f89SLORTET        string $mediaID,
439da933f89SLORTET        string $size = 'bigorsmall',
440da933f89SLORTET        array $params = ['width' => 55],
44174a9e763SLORTET        ?int &$mtime = null,
44274a9e763SLORTET        bool $withDefault = false
443da933f89SLORTET    ) {
44474a9e763SLORTET        $iconMediaID = $this->getMediaIconId($mediaID, $size);
445da933f89SLORTET        if (!$iconMediaID) {
44674a9e763SLORTET            if ($withDefault) {
44774a9e763SLORTET                return $this->getDefaultIconUrl($params, $mtime);
44874a9e763SLORTET            }
449da933f89SLORTET            $mtime = 0;
450da933f89SLORTET            return false;
451da933f89SLORTET        }
452da933f89SLORTET
453da933f89SLORTET        $mtime = $this->getMediaMTime((string)$iconMediaID);
454da933f89SLORTET        $url = (string)ml((string)$iconMediaID, $params);
455da933f89SLORTET        if ($url === '') return false;
456da933f89SLORTET        return $this->appendVersionToUrl($url, $mtime);
457da933f89SLORTET    }
458da933f89SLORTET
459*b603bbe1SLORTET    /**
460*b603bbe1SLORTET     * Added in version 2026-03-06.
461*b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
462*b603bbe1SLORTET     * Use getMediaIconUrl() instead.
463*b603bbe1SLORTET     */
46474a9e763SLORTET    public function getMediaIcon(
46574a9e763SLORTET        string $mediaID,
46674a9e763SLORTET        string $size = 'bigorsmall',
46774a9e763SLORTET        array $params = ['width' => 55],
46874a9e763SLORTET        ?int &$mtime = null,
46974a9e763SLORTET        bool $withDefault = false
47074a9e763SLORTET    ) {
47174a9e763SLORTET        return $this->getMediaIconUrl($mediaID, $size, $params, $mtime, $withDefault);
47274a9e763SLORTET    }
47374a9e763SLORTET
474*b603bbe1SLORTET    /**
475*b603bbe1SLORTET     * Added in version 2026-03-06.
476*b603bbe1SLORTET     * Returns the icon management URL associated with a media file, or null when unavailable.
477*b603bbe1SLORTET     */
478*b603bbe1SLORTET    public function getUploadMediaIconPage(string $mediaID = '') {
479da933f89SLORTET        $mediaID = cleanID($mediaID);
480da933f89SLORTET        if ($mediaID === '') return null;
481da933f89SLORTET
482da933f89SLORTET        $namespace = getNS($mediaID);
483da933f89SLORTET        $filename = noNS($mediaID);
484da933f89SLORTET        $base = (string)pathinfo($filename, PATHINFO_FILENAME);
485da933f89SLORTET        $targetPage = cleanID($namespace !== '' ? ($namespace . ':' . $base) : $base);
486da933f89SLORTET        if ($targetPage === '') return null;
487da933f89SLORTET
488da933f89SLORTET        return $this->getUploadIconPage($targetPage);
489da933f89SLORTET    }
490da933f89SLORTET}
491