xref: /plugin/pagesicon/helper.php (revision a3ea184d8fbbe3d7b0907bb9f539dee5a6e56ad6)
1da933f89SLORTET<?php
2da933f89SLORTETif (!defined('DOKU_INC')) die();
3da933f89SLORTET
4437e13a7SLORTETclass helper_plugin_pagesicon extends DokuWiki_Plugin {
5b603bbe1SLORTET    private const BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH = 'lib/plugins/pagesicon/images/default_image.png';
6b603bbe1SLORTET
7b603bbe1SLORTET    private function getBundledDefaultImagePath(): string {
8b603bbe1SLORTET        return DOKU_INC . self::BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH;
974a9e763SLORTET    }
1074a9e763SLORTET
11b603bbe1SLORTET    private function getBundledDefaultImageUrl(): string {
1274a9e763SLORTET        $path = $this->getBundledDefaultImagePath();
1374a9e763SLORTET        if (!@file_exists($path)) return '';
1474a9e763SLORTET
1574a9e763SLORTET        $base = rtrim((string)DOKU_BASE, '/');
16b603bbe1SLORTET        $url = $base . '/' . self::BUNDLED_DEFAULT_IMAGE_RELATIVE_PATH;
1774a9e763SLORTET        $mtime = @filemtime($path);
1874a9e763SLORTET        return $this->appendVersionToUrl($url, $mtime ? (int)$mtime : 0);
1974a9e763SLORTET    }
2074a9e763SLORTET
21b603bbe1SLORTET    private function getConfiguredDefaultImageMediaID() {
2274a9e763SLORTET        $mediaID = cleanID((string)$this->getConf('default_image'));
2374a9e763SLORTET        if ($mediaID === '') return false;
2474a9e763SLORTET        if (!@file_exists(mediaFN($mediaID))) return false;
2574a9e763SLORTET        return $mediaID;
2674a9e763SLORTET    }
2774a9e763SLORTET
28b603bbe1SLORTET    private function getMediaMTime(string $mediaID): int {
29da933f89SLORTET        $mediaID = cleanID($mediaID);
30da933f89SLORTET        if ($mediaID === '') return 0;
31da933f89SLORTET        $file = mediaFN($mediaID);
32da933f89SLORTET        if (!@file_exists($file)) return 0;
33da933f89SLORTET        $mtime = @filemtime($file);
34da933f89SLORTET        return $mtime ? (int)$mtime : 0;
35da933f89SLORTET    }
36da933f89SLORTET
37b603bbe1SLORTET    private function appendVersionToUrl(string $url, int $mtime): string {
38da933f89SLORTET        if ($url === '' || $mtime <= 0) return $url;
39da933f89SLORTET        $sep = strpos($url, '?') === false ? '?' : '&';
40da933f89SLORTET        return $url . $sep . 'pi_ts=' . $mtime;
41da933f89SLORTET    }
42da933f89SLORTET
43b603bbe1SLORTET    /**
44b603bbe1SLORTET     * Added in version 2026-03-06.
45b603bbe1SLORTET     * Notifies consumers that an icon changed and triggers cache invalidation hooks.
46b603bbe1SLORTET     */
47b603bbe1SLORTET    public function notifyIconUpdated(string $targetPage, string $action = 'update', string $mediaID = ''): void {
48da933f89SLORTET        global $conf;
49da933f89SLORTET
50da933f89SLORTET        @io_saveFile($conf['cachedir'] . '/purgefile', time());
51da933f89SLORTET
52da933f89SLORTET        $data = [
53da933f89SLORTET            'target_page' => cleanID($targetPage),
54da933f89SLORTET            'action' => $action,
55da933f89SLORTET            'media_id' => cleanID($mediaID),
56da933f89SLORTET        ];
57da933f89SLORTET        \dokuwiki\Extension\Event::createAndTrigger('PLUGIN_PAGESICON_UPDATED', $data);
58da933f89SLORTET    }
59da933f89SLORTET
60b603bbe1SLORTET    /**
61b603bbe1SLORTET     * Added in version 2026-03-11.
62b603bbe1SLORTET     * Returns the configured filename templates for the requested icon variant.
63b603bbe1SLORTET     */
64b603bbe1SLORTET    public function getVariantTemplates(string $variant): array {
65b603bbe1SLORTET        $confKey = $variant === 'small' ? 'icon_thumbnail_name' : 'icon_name';
66b603bbe1SLORTET        $raw = (string)$this->getConf($confKey);
67b603bbe1SLORTET
68b603bbe1SLORTET        if (trim($raw) === '') {
69b603bbe1SLORTET            trigger_error('pagesicon: missing required configuration "' . $confKey . '"', E_USER_WARNING);
70b603bbe1SLORTET            return [];
71b603bbe1SLORTET        }
72b603bbe1SLORTET
73b603bbe1SLORTET        $templates = array_values(array_unique(array_filter(array_map('trim', explode(';', $raw)))));
74b603bbe1SLORTET        if (!$templates) {
75b603bbe1SLORTET            trigger_error('pagesicon: configuration "' . $confKey . '" does not contain any usable value', E_USER_WARNING);
76b603bbe1SLORTET        }
77b603bbe1SLORTET
78b603bbe1SLORTET        return $templates;
79b603bbe1SLORTET    }
80b603bbe1SLORTET
81b603bbe1SLORTET    /**
82b603bbe1SLORTET     * Added in version 2026-03-11.
83b603bbe1SLORTET     * Normalizes an icon filename candidate to its base media name without namespace or extension.
84b603bbe1SLORTET     */
85b603bbe1SLORTET    public function normalizeIconBaseName(string $name): string {
86b603bbe1SLORTET        $name = trim($name);
87b603bbe1SLORTET        if ($name === '') return '';
88b603bbe1SLORTET        $name = noNS($name);
89b603bbe1SLORTET        $name = preg_replace('/\.[a-z0-9]+$/i', '', $name);
90b603bbe1SLORTET        $name = cleanID($name);
91b603bbe1SLORTET        return str_replace(':', '', $name);
92b603bbe1SLORTET    }
93b603bbe1SLORTET
94b603bbe1SLORTET    /**
95b603bbe1SLORTET     * Added in version 2026-03-11.
96b603bbe1SLORTET     * Returns the allowed target base names for an upload, indexed by their normalized value.
97b603bbe1SLORTET     */
98b603bbe1SLORTET    public function getUploadNameChoices(string $targetPage, string $variant): array {
99b603bbe1SLORTET        $pageID = noNS($targetPage);
100b603bbe1SLORTET        $choices = [];
101b603bbe1SLORTET
102b603bbe1SLORTET        foreach ($this->getVariantTemplates($variant) as $tpl) {
103b603bbe1SLORTET            $resolved = str_replace('~pagename~', $pageID, $tpl);
104b603bbe1SLORTET            $base = $this->normalizeIconBaseName($resolved);
105b603bbe1SLORTET            if ($base === '') continue;
106b603bbe1SLORTET            $choices[$base] = $base . '.ext';
107b603bbe1SLORTET        }
108b603bbe1SLORTET
109b603bbe1SLORTET        return $choices;
110b603bbe1SLORTET    }
111b603bbe1SLORTET
112b603bbe1SLORTET    private function buildConfiguredCandidatesFromRaw(string $raw, string $namespace, string $pageID): array {
113da933f89SLORTET        $configured = [];
114da933f89SLORTET        $entries = array_filter(array_map('trim', explode(';', $raw)));
115da933f89SLORTET
116da933f89SLORTET        foreach ($entries as $entry) {
117da933f89SLORTET            $name = str_replace('~pagename~', $pageID, $entry);
118da933f89SLORTET            if ($name === '') continue;
119da933f89SLORTET
120da933f89SLORTET            if (strpos($name, ':') === false && $namespace !== '') {
121da933f89SLORTET                $configured[] = $namespace . ':' . $name;
122da933f89SLORTET            } else {
123da933f89SLORTET                $configured[] = ltrim($name, ':');
124da933f89SLORTET            }
125da933f89SLORTET        }
126da933f89SLORTET
127da933f89SLORTET        return array_values(array_unique($configured));
128da933f89SLORTET    }
129da933f89SLORTET
130b603bbe1SLORTET    private function buildConfiguredCandidates(string $namespace, string $pageID, string $sizeMode): array {
131da933f89SLORTET        $bigRaw = trim((string)$this->getConf('icon_name'));
132da933f89SLORTET        $smallRaw = trim((string)$this->getConf('icon_thumbnail_name'));
133da933f89SLORTET
134da933f89SLORTET        $big = $this->buildConfiguredCandidatesFromRaw($bigRaw, $namespace, $pageID);
135da933f89SLORTET        $small = $this->buildConfiguredCandidatesFromRaw($smallRaw, $namespace, $pageID);
136da933f89SLORTET
137da933f89SLORTET        if ($sizeMode === 'big') return $big;
138da933f89SLORTET        if ($sizeMode === 'small') return $small;
139da933f89SLORTET        if ($sizeMode === 'smallorbig') return array_values(array_unique(array_merge($small, $big)));
140da933f89SLORTET
141da933f89SLORTET        // Default: bigorsmall
142da933f89SLORTET        return array_values(array_unique(array_merge($big, $small)));
143da933f89SLORTET    }
144da933f89SLORTET
145b603bbe1SLORTET    private function normalizeSizeMode(string $size): string {
146da933f89SLORTET        $size = strtolower(trim($size));
147da933f89SLORTET        $allowed = ['big', 'small', 'bigorsmall', 'smallorbig'];
148da933f89SLORTET        if (in_array($size, $allowed, true)) return $size;
149da933f89SLORTET        return 'bigorsmall';
150da933f89SLORTET    }
151da933f89SLORTET
152b603bbe1SLORTET    /**
153b603bbe1SLORTET     * Added in version 2026-03-11.
154b603bbe1SLORTET     * Returns the configured list of allowed icon file extensions.
155b603bbe1SLORTET     */
156b603bbe1SLORTET    public function getConfiguredExtensions(): array {
157da933f89SLORTET        $raw = trim((string)$this->getConf('extensions'));
158b603bbe1SLORTET        if ($raw === '') {
159b603bbe1SLORTET            trigger_error('pagesicon: missing required configuration "extensions"', E_USER_WARNING);
160b603bbe1SLORTET            return [];
161b603bbe1SLORTET        }
162da933f89SLORTET
163da933f89SLORTET        $extensions = array_values(array_unique(array_filter(array_map(function ($ext) {
164da933f89SLORTET            return strtolower(ltrim(trim((string)$ext), '.'));
165da933f89SLORTET        }, explode(';', $raw)))));
166da933f89SLORTET
167b603bbe1SLORTET        if (!$extensions) {
168b603bbe1SLORTET            trigger_error('pagesicon: configuration "extensions" does not contain any usable value', E_USER_WARNING);
169da933f89SLORTET        }
170da933f89SLORTET
171b603bbe1SLORTET        return $extensions;
172b603bbe1SLORTET    }
173b603bbe1SLORTET
174b603bbe1SLORTET    private function hasKnownExtension(string $name, array $extensions): bool {
175da933f89SLORTET        $fileExt = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
176da933f89SLORTET        return $fileExt !== '' && in_array($fileExt, $extensions, true);
177da933f89SLORTET    }
178da933f89SLORTET
179*a3ea184dSLORTET    private function getParentFallbackMode(): string {
180*a3ea184dSLORTET        $mode = strtolower(trim((string)$this->getConf('parent_fallback')));
181*a3ea184dSLORTET        if ($mode !== 'direct' && $mode !== 'first') return 'none';
182*a3ea184dSLORTET        return $mode;
183*a3ea184dSLORTET    }
184*a3ea184dSLORTET
185*a3ea184dSLORTET    private function resolveOwnPageIconId(string $namespace, string $pageID, string $sizeMode, array $extensions) {
186da933f89SLORTET        $namespace = $namespace ?: '';
187da933f89SLORTET        $pageBase = $namespace ? ($namespace . ':' . $pageID) : $pageID;
188da933f89SLORTET        $nsBase = $namespace ? ($namespace . ':') : '';
189da933f89SLORTET
190da933f89SLORTET        $genericBig = [
191da933f89SLORTET            $pageBase,
192da933f89SLORTET            $pageBase . ':logo',
193da933f89SLORTET            $nsBase . 'logo',
194da933f89SLORTET        ];
195da933f89SLORTET        $genericSmall = [
196da933f89SLORTET            $pageBase . ':thumbnail',
197da933f89SLORTET            $nsBase . 'thumbnail',
198da933f89SLORTET        ];
199da933f89SLORTET
200da933f89SLORTET        if ($sizeMode === 'big') {
201da933f89SLORTET            $generic = $genericBig;
202da933f89SLORTET        } elseif ($sizeMode === 'small') {
203da933f89SLORTET            $generic = $genericSmall;
204da933f89SLORTET        } elseif ($sizeMode === 'smallorbig') {
205da933f89SLORTET            $generic = array_merge($genericSmall, $genericBig);
206da933f89SLORTET        } else {
207da933f89SLORTET            $generic = array_merge($genericBig, $genericSmall);
208da933f89SLORTET        }
209da933f89SLORTET
210da933f89SLORTET        $imageNames = array_merge($this->buildConfiguredCandidates($namespace, $pageID, $sizeMode), $generic);
211da933f89SLORTET
212da933f89SLORTET        foreach ($imageNames as $name) {
213da933f89SLORTET            if ($this->hasKnownExtension($name, $extensions)) {
214da933f89SLORTET                if (@file_exists(mediaFN($name))) return $name;
215da933f89SLORTET                continue;
216da933f89SLORTET            }
217da933f89SLORTET
218da933f89SLORTET            foreach ($extensions as $ext) {
219da933f89SLORTET                $path = $name . '.' . $ext;
220da933f89SLORTET                if (@file_exists(mediaFN($path))) return $path;
221da933f89SLORTET            }
222da933f89SLORTET        }
223da933f89SLORTET
224da933f89SLORTET        return false;
225da933f89SLORTET    }
226da933f89SLORTET
227*a3ea184dSLORTET    private function resolveNamespacePageIconId(string $namespace, string $sizeMode, array $extensions) {
228*a3ea184dSLORTET        global $conf;
229*a3ea184dSLORTET
230*a3ea184dSLORTET        $namespace = cleanID($namespace);
231*a3ea184dSLORTET        if ($namespace === '') return false;
232*a3ea184dSLORTET
233*a3ea184dSLORTET        $parentNamespace = (string)(getNS($namespace) ?: '');
234*a3ea184dSLORTET        $pageID = noNS($namespace);
235*a3ea184dSLORTET
236*a3ea184dSLORTET        $iconID = $this->resolveOwnPageIconId($parentNamespace, $pageID, $sizeMode, $extensions);
237*a3ea184dSLORTET        if ($iconID) return $iconID;
238*a3ea184dSLORTET
239*a3ea184dSLORTET        $leafPageID = cleanID($namespace . ':' . $pageID);
240*a3ea184dSLORTET        if ($leafPageID !== '' && page_exists($leafPageID)) {
241*a3ea184dSLORTET            $iconID = $this->resolveOwnPageIconId($namespace, $pageID, $sizeMode, $extensions);
242*a3ea184dSLORTET            if ($iconID) return $iconID;
243*a3ea184dSLORTET        }
244*a3ea184dSLORTET
245*a3ea184dSLORTET        if (isset($conf['start'])) {
246*a3ea184dSLORTET            $startId = cleanID((string)$conf['start']);
247*a3ea184dSLORTET            if ($startId !== '') {
248*a3ea184dSLORTET                $iconID = $this->resolveOwnPageIconId($namespace, $startId, $sizeMode, $extensions);
249*a3ea184dSLORTET                if ($iconID) return $iconID;
250*a3ea184dSLORTET            }
251*a3ea184dSLORTET        }
252*a3ea184dSLORTET
253*a3ea184dSLORTET        return false;
254*a3ea184dSLORTET    }
255*a3ea184dSLORTET
256*a3ea184dSLORTET    /**
257*a3ea184dSLORTET     * Added in version 2026-03-09.
258*a3ea184dSLORTET     * Resolves the icon media ID for a page, or false when no icon matches.
259*a3ea184dSLORTET     * Replaces the older getPageImage() name.
260*a3ea184dSLORTET     */
261*a3ea184dSLORTET    public function getPageIconId(
262*a3ea184dSLORTET        string $namespace,
263*a3ea184dSLORTET        string $pageID,
264*a3ea184dSLORTET        string $size = 'bigorsmall'
265*a3ea184dSLORTET    )
266*a3ea184dSLORTET    {
267*a3ea184dSLORTET        $sizeMode = $this->normalizeSizeMode($size);
268*a3ea184dSLORTET        $extensions = $this->getConfiguredExtensions();
269*a3ea184dSLORTET        $iconID = $this->resolveOwnPageIconId($namespace, $pageID, $sizeMode, $extensions);
270*a3ea184dSLORTET        if ($iconID) return $iconID;
271*a3ea184dSLORTET
272*a3ea184dSLORTET        $fallbackMode = $this->getParentFallbackMode();
273*a3ea184dSLORTET        if ($fallbackMode === 'none') return false;
274*a3ea184dSLORTET
275*a3ea184dSLORTET        $currentNamespace = $namespace ?: '';
276*a3ea184dSLORTET        while ($currentNamespace !== '') {
277*a3ea184dSLORTET            $parentNamespace = (string)(getNS($currentNamespace) ?: '');
278*a3ea184dSLORTET            $lookupNamespace = $parentNamespace !== '' ? $parentNamespace : $currentNamespace;
279*a3ea184dSLORTET            $iconID = $this->resolveNamespacePageIconId($lookupNamespace, $sizeMode, $extensions);
280*a3ea184dSLORTET            if ($iconID) return $iconID;
281*a3ea184dSLORTET            if ($fallbackMode === 'direct' || $parentNamespace === '') break;
282*a3ea184dSLORTET            $currentNamespace = $parentNamespace;
283*a3ea184dSLORTET        }
284*a3ea184dSLORTET
285*a3ea184dSLORTET        return false;
286*a3ea184dSLORTET    }
287*a3ea184dSLORTET
288b603bbe1SLORTET    /**
289b603bbe1SLORTET     * Added in version 2026-03-06.
290b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
291b603bbe1SLORTET     * Use getPageIconId() instead.
292b603bbe1SLORTET     */
29374a9e763SLORTET    public function getPageImage(
29474a9e763SLORTET        string $namespace,
29574a9e763SLORTET        string $pageID,
29674a9e763SLORTET        string $size = 'bigorsmall',
29774a9e763SLORTET        bool $withDefault = false
29874a9e763SLORTET    ) {
29974a9e763SLORTET        return $this->getPageIconId($namespace, $pageID, $size);
30074a9e763SLORTET    }
30174a9e763SLORTET
302b603bbe1SLORTET    /**
303b603bbe1SLORTET     * Added in version 2026-03-06.
304b603bbe1SLORTET     * Returns the icon management URL for a page, or null when upload is not allowed.
305b603bbe1SLORTET     */
306b603bbe1SLORTET    public function getUploadIconPage(string $targetPage = '') {
307da933f89SLORTET        global $ID;
308da933f89SLORTET
309da933f89SLORTET        $targetPage = cleanID($targetPage);
310da933f89SLORTET        if ($targetPage === '') {
311da933f89SLORTET            $targetPage = cleanID(getNS((string)$ID));
312da933f89SLORTET        }
313da933f89SLORTET        if ($targetPage === '') {
314da933f89SLORTET            $targetPage = cleanID((string)$ID);
315da933f89SLORTET        }
316da933f89SLORTET        if ($targetPage === '') return null;
317da933f89SLORTET
318da933f89SLORTET        if (auth_quickaclcheck($targetPage) < AUTH_UPLOAD) {
319da933f89SLORTET            return null;
320da933f89SLORTET        }
321da933f89SLORTET
322da933f89SLORTET        return wl($targetPage, ['do' => 'pagesicon']);
323da933f89SLORTET    }
324da933f89SLORTET
325b603bbe1SLORTET    /**
326b603bbe1SLORTET     * Added in version 2026-03-09.
327b603bbe1SLORTET     * Resolves the icon media ID associated with a media file, or false when none matches.
328b603bbe1SLORTET     * Replaces the older getMediaImage() name.
329b603bbe1SLORTET     */
330b603bbe1SLORTET    public function getMediaIconId(string $mediaID, string $size = 'bigorsmall') {
331da933f89SLORTET        $mediaID = cleanID($mediaID);
332da933f89SLORTET        if ($mediaID === '') return false;
333da933f89SLORTET
334da933f89SLORTET        $namespace = getNS($mediaID);
335da933f89SLORTET        $filename = noNS($mediaID);
336da933f89SLORTET        $base = (string)pathinfo($filename, PATHINFO_FILENAME);
337da933f89SLORTET        $pageID = cleanID($base);
338da933f89SLORTET        if ($pageID === '') return false;
339da933f89SLORTET
34074a9e763SLORTET        return $this->getPageIconId($namespace, $pageID, $size);
341da933f89SLORTET    }
342da933f89SLORTET
343b603bbe1SLORTET    /**
344b603bbe1SLORTET     * Added in version 2026-03-06.
345b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
346b603bbe1SLORTET     * Use getMediaIconId() instead.
347b603bbe1SLORTET     */
348b603bbe1SLORTET    public function getMediaImage(string $mediaID, string $size = 'bigorsmall', bool $withDefault = false) {
34974a9e763SLORTET        return $this->getMediaIconId($mediaID, $size);
35074a9e763SLORTET    }
35174a9e763SLORTET
352b603bbe1SLORTET    private function matchesPageIconVariant(string $mediaID, string $namespace, string $pageID): bool {
353b603bbe1SLORTET        $bigIconID = $this->getPageIconId($namespace, $pageID, 'big');
354b603bbe1SLORTET        if ($bigIconID && cleanID((string)$bigIconID) === $mediaID) return true;
355b603bbe1SLORTET
356b603bbe1SLORTET        $smallIconID = $this->getPageIconId($namespace, $pageID, 'small');
357b603bbe1SLORTET        if ($smallIconID && cleanID((string)$smallIconID) === $mediaID) return true;
358b603bbe1SLORTET
359b603bbe1SLORTET        return false;
360b603bbe1SLORTET    }
361b603bbe1SLORTET
362b603bbe1SLORTET    /**
363b603bbe1SLORTET     * Added in version 2026-03-11.
364b603bbe1SLORTET     * Checks whether a media ID should be considered a page icon managed by the pagesicon plugin.
365b603bbe1SLORTET     */
366b603bbe1SLORTET    public function isPageIconMedia(string $mediaID): bool {
367b603bbe1SLORTET        global $conf;
368b603bbe1SLORTET
369b603bbe1SLORTET        $mediaID = cleanID($mediaID);
370b603bbe1SLORTET        if ($mediaID === '') return false;
371b603bbe1SLORTET
372b603bbe1SLORTET        $namespace = getNS($mediaID);
373b603bbe1SLORTET        $filename = noNS($mediaID);
374b603bbe1SLORTET        $basename = cleanID((string)pathinfo($filename, PATHINFO_FILENAME));
375b603bbe1SLORTET        if ($basename === '') return false;
376b603bbe1SLORTET
377b603bbe1SLORTET        // Case 1: this media is the big or small icon selected for a page with the same base name.
378b603bbe1SLORTET        $sameNamePageID = $namespace !== '' ? ($namespace . ':' . $basename) : $basename;
379b603bbe1SLORTET        if (page_exists($sameNamePageID)) {
380b603bbe1SLORTET            if ($this->matchesPageIconVariant($mediaID, $namespace, $basename)) return true;
381b603bbe1SLORTET        }
382b603bbe1SLORTET
383b603bbe1SLORTET        // Case 2: this media is the big or small icon selected for a page whose ID matches the namespace.
384b603bbe1SLORTET        if ($namespace !== '' && page_exists($namespace)) {
385b603bbe1SLORTET            $parentNamespace = getNS($namespace);
386b603bbe1SLORTET            $pageID = noNS($namespace);
387b603bbe1SLORTET            if ($this->matchesPageIconVariant($mediaID, $parentNamespace, $pageID)) return true;
388b603bbe1SLORTET        }
389b603bbe1SLORTET
390b603bbe1SLORTET        // Case 3: this media is the big or small icon selected for a page whose ID
391b603bbe1SLORTET        // matches the namespace leaf, for example "...:playground:playground".
392b603bbe1SLORTET        if ($namespace !== '') {
393b603bbe1SLORTET            $namespaceLeaf = noNS($namespace);
394b603bbe1SLORTET            $leafPageID = cleanID($namespace . ':' . $namespaceLeaf);
395b603bbe1SLORTET            if ($leafPageID !== '' && page_exists($leafPageID)) {
396b603bbe1SLORTET                if ($this->matchesPageIconVariant($mediaID, $namespace, $namespaceLeaf)) return true;
397b603bbe1SLORTET            }
398b603bbe1SLORTET        }
399b603bbe1SLORTET
400b603bbe1SLORTET        // Case 4: this media is the big or small icon selected for the namespace start page
401b603bbe1SLORTET        // (for example "...:start"), which often carries the visible page content.
402b603bbe1SLORTET        if ($namespace !== '' && isset($conf['start'])) {
403b603bbe1SLORTET            $startId = cleanID((string)$conf['start']);
404b603bbe1SLORTET            $startPage = $startId !== '' ? cleanID($namespace . ':' . $startId) : '';
405b603bbe1SLORTET            if ($startPage !== '' && page_exists($startPage)) {
406b603bbe1SLORTET                if ($this->matchesPageIconVariant($mediaID, $namespace, noNS($startPage))) return true;
407b603bbe1SLORTET            }
408b603bbe1SLORTET        }
409b603bbe1SLORTET
410b603bbe1SLORTET        return false;
411b603bbe1SLORTET    }
412b603bbe1SLORTET
413b603bbe1SLORTET    /**
414b603bbe1SLORTET     * Added in version 2026-03-09.
415b603bbe1SLORTET     * Returns the configured default icon URL, or the bundled fallback image when available.
416b603bbe1SLORTET     */
417b603bbe1SLORTET    public function getDefaultIconUrl(array $params = ['width' => 55], ?int &$mtime = null) {
41874a9e763SLORTET        $mediaID = $this->getConfiguredDefaultImageMediaID();
41974a9e763SLORTET        if ($mediaID) {
42074a9e763SLORTET            $mtime = $this->getMediaMTime((string)$mediaID);
42174a9e763SLORTET            $url = (string)ml((string)$mediaID, $params);
42274a9e763SLORTET            if ($url === '') return false;
42374a9e763SLORTET            return $this->appendVersionToUrl($url, $mtime);
42474a9e763SLORTET        }
42574a9e763SLORTET
42674a9e763SLORTET        $mtime = 0;
42774a9e763SLORTET        $bundled = $this->getBundledDefaultImageUrl();
42874a9e763SLORTET        if ($bundled !== '') return $bundled;
42974a9e763SLORTET
43074a9e763SLORTET        return false;
43174a9e763SLORTET    }
43274a9e763SLORTET
433b603bbe1SLORTET    /**
434b603bbe1SLORTET     * Added in version 2026-03-09.
435b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
436b603bbe1SLORTET     * Use getDefaultIconUrl() instead.
437b603bbe1SLORTET     */
438b603bbe1SLORTET    public function getDefaultImageIcon(array $params = ['width' => 55], ?int &$mtime = null) {
43974a9e763SLORTET        return $this->getDefaultIconUrl($params, $mtime);
44074a9e763SLORTET    }
44174a9e763SLORTET
442b603bbe1SLORTET    /**
443b603bbe1SLORTET     * Added in version 2026-03-09.
444b603bbe1SLORTET     * Returns a versioned icon URL for a page, or false when no icon matches.
445b603bbe1SLORTET     * Replaces the older getImageIcon() name.
446b603bbe1SLORTET     */
44774a9e763SLORTET    public function getPageIconUrl(
448da933f89SLORTET        string $namespace,
449da933f89SLORTET        string $pageID,
450da933f89SLORTET        string $size = 'bigorsmall',
451da933f89SLORTET        array $params = ['width' => 55],
45274a9e763SLORTET        ?int &$mtime = null,
45374a9e763SLORTET        bool $withDefault = false
454da933f89SLORTET    ) {
45574a9e763SLORTET        $mediaID = $this->getPageIconId($namespace, $pageID, $size);
456da933f89SLORTET        if (!$mediaID) {
45774a9e763SLORTET            if ($withDefault) {
45874a9e763SLORTET                return $this->getDefaultIconUrl($params, $mtime);
45974a9e763SLORTET            }
460da933f89SLORTET            $mtime = 0;
461da933f89SLORTET            return false;
462da933f89SLORTET        }
463da933f89SLORTET
464da933f89SLORTET        $mtime = $this->getMediaMTime((string)$mediaID);
465da933f89SLORTET        $url = (string)ml((string)$mediaID, $params);
466da933f89SLORTET        if ($url === '') return false;
467da933f89SLORTET        return $this->appendVersionToUrl($url, $mtime);
468da933f89SLORTET    }
469da933f89SLORTET
470b603bbe1SLORTET    /**
471b603bbe1SLORTET     * Added in version 2026-03-06.
472b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
473b603bbe1SLORTET     * Use getPageIconUrl() instead.
474b603bbe1SLORTET     */
47574a9e763SLORTET    public function getImageIcon(
47674a9e763SLORTET        string $namespace,
47774a9e763SLORTET        string $pageID,
47874a9e763SLORTET        string $size = 'bigorsmall',
47974a9e763SLORTET        array $params = ['width' => 55],
48074a9e763SLORTET        ?int &$mtime = null,
48174a9e763SLORTET        bool $withDefault = false
48274a9e763SLORTET    ) {
48374a9e763SLORTET        return $this->getPageIconUrl($namespace, $pageID, $size, $params, $mtime, $withDefault);
48474a9e763SLORTET    }
48574a9e763SLORTET
486b603bbe1SLORTET    /**
487b603bbe1SLORTET     * Added in version 2026-03-09.
488b603bbe1SLORTET     * Returns a versioned icon URL for a media file, or false when no icon matches.
489b603bbe1SLORTET     * Replaces the older getMediaIcon() name.
490b603bbe1SLORTET     */
49174a9e763SLORTET    public function getMediaIconUrl(
492da933f89SLORTET        string $mediaID,
493da933f89SLORTET        string $size = 'bigorsmall',
494da933f89SLORTET        array $params = ['width' => 55],
49574a9e763SLORTET        ?int &$mtime = null,
49674a9e763SLORTET        bool $withDefault = false
497da933f89SLORTET    ) {
49874a9e763SLORTET        $iconMediaID = $this->getMediaIconId($mediaID, $size);
499da933f89SLORTET        if (!$iconMediaID) {
50074a9e763SLORTET            if ($withDefault) {
50174a9e763SLORTET                return $this->getDefaultIconUrl($params, $mtime);
50274a9e763SLORTET            }
503da933f89SLORTET            $mtime = 0;
504da933f89SLORTET            return false;
505da933f89SLORTET        }
506da933f89SLORTET
507da933f89SLORTET        $mtime = $this->getMediaMTime((string)$iconMediaID);
508da933f89SLORTET        $url = (string)ml((string)$iconMediaID, $params);
509da933f89SLORTET        if ($url === '') return false;
510da933f89SLORTET        return $this->appendVersionToUrl($url, $mtime);
511da933f89SLORTET    }
512da933f89SLORTET
513b603bbe1SLORTET    /**
514b603bbe1SLORTET     * Added in version 2026-03-06.
515b603bbe1SLORTET     * Deprecated since version 2026-03-09, kept for backward compatibility.
516b603bbe1SLORTET     * Use getMediaIconUrl() instead.
517b603bbe1SLORTET     */
51874a9e763SLORTET    public function getMediaIcon(
51974a9e763SLORTET        string $mediaID,
52074a9e763SLORTET        string $size = 'bigorsmall',
52174a9e763SLORTET        array $params = ['width' => 55],
52274a9e763SLORTET        ?int &$mtime = null,
52374a9e763SLORTET        bool $withDefault = false
52474a9e763SLORTET    ) {
52574a9e763SLORTET        return $this->getMediaIconUrl($mediaID, $size, $params, $mtime, $withDefault);
52674a9e763SLORTET    }
52774a9e763SLORTET
528b603bbe1SLORTET    /**
529b603bbe1SLORTET     * Added in version 2026-03-06.
530b603bbe1SLORTET     * Returns the icon management URL associated with a media file, or null when unavailable.
531b603bbe1SLORTET     */
532b603bbe1SLORTET    public function getUploadMediaIconPage(string $mediaID = '') {
533da933f89SLORTET        $mediaID = cleanID($mediaID);
534da933f89SLORTET        if ($mediaID === '') return null;
535da933f89SLORTET
536da933f89SLORTET        $namespace = getNS($mediaID);
537da933f89SLORTET        $filename = noNS($mediaID);
538da933f89SLORTET        $base = (string)pathinfo($filename, PATHINFO_FILENAME);
539da933f89SLORTET        $targetPage = cleanID($namespace !== '' ? ($namespace . ':' . $base) : $base);
540da933f89SLORTET        if ($targetPage === '') return null;
541da933f89SLORTET
542da933f89SLORTET        return $this->getUploadIconPage($targetPage);
543da933f89SLORTET    }
544da933f89SLORTET}
545