xref: /plugin/pagesicon/action.php (revision c8e99a276375ecec8f29dd837c92b2b86eeb3f41)
1da933f89SLORTET<?php
2da933f89SLORTETif(!defined('DOKU_INC')) die();
3b603bbe1SLORTETif(!defined('DOKU_MEDIAMANAGER_URL_BASE')) define('DOKU_MEDIAMANAGER_URL_BASE', DOKU_BASE . 'lib/exe/mediamanager.php');
4da933f89SLORTET
5da933f89SLORTETclass action_plugin_pagesicon extends DokuWiki_Action_Plugin {
6da933f89SLORTET	public function register(Doku_Event_Handler $controller) {
719821f1cSLORTET		$controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'displayPageIcon');
8*c8e99a27SLORTET		$controller->register_hook('RENDERER_CONTENT_POSTPROCESS', 'AFTER', $this, 'injectLinkIcons');
9da933f89SLORTET		$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'setPageFavicon');
10b603bbe1SLORTET		$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addUploadFormScript');
11b603bbe1SLORTET		$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addFaviconRuntimeScript');
12da933f89SLORTET		$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleAction');
13da933f89SLORTET		$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'renderAction');
14da933f89SLORTET		$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addPageAction');
15da933f89SLORTET	}
16da933f89SLORTET
17b603bbe1SLORTET	public function addPageAction(Doku_Event $event): void {
18da933f89SLORTET		global $ID;
19da933f89SLORTET
20da933f89SLORTET		if (($event->data['view'] ?? '') !== 'page') return;
21437e13a7SLORTET		if ($this->isActionDisabled('pagesicon')) return;
22da933f89SLORTET		if (auth_quickaclcheck((string)$ID) < AUTH_UPLOAD) return;
23da933f89SLORTET
24da933f89SLORTET		foreach (($event->data['items'] ?? []) as $item) {
25da933f89SLORTET			if ($item instanceof \dokuwiki\Menu\Item\AbstractItem && $item->getType() === 'pagesicon') {
26da933f89SLORTET				return;
27da933f89SLORTET			}
28da933f89SLORTET		}
29da933f89SLORTET
30da933f89SLORTET		$label = (string)$this->getLang('page_action');
31da933f89SLORTET		if ($label === '') $label = 'Gerer l\'icone';
32da933f89SLORTET		$title = (string)$this->getLang('page_action_title');
33da933f89SLORTET		if ($title === '') $title = $label;
34da933f89SLORTET		$targetPage = cleanID((string)$ID);
35da933f89SLORTET
36da933f89SLORTET		$event->data['items'][] = new class($targetPage, $label, $title) extends \dokuwiki\Menu\Item\AbstractItem {
37b603bbe1SLORTET			public function __construct(string $targetPage, string $label, string $title) {
38da933f89SLORTET				parent::__construct();
39da933f89SLORTET				$this->type = 'pagesicon';
40da933f89SLORTET				$this->id = $targetPage;
41da933f89SLORTET				$this->params = [
42da933f89SLORTET					'do' => 'pagesicon',
43da933f89SLORTET				];
44da933f89SLORTET				$this->label = $label;
45da933f89SLORTET				$this->title = $title;
46da933f89SLORTET				$this->svg = DOKU_INC . 'lib/images/menu/folder-multiple-image.svg';
47da933f89SLORTET			}
48da933f89SLORTET		};
49da933f89SLORTET	}
50da933f89SLORTET
51da933f89SLORTET	private function getIconSize(): int {
52b603bbe1SLORTET		return (int)$this->getConf('icon_size');
53da933f89SLORTET	}
54da933f89SLORTET
55437e13a7SLORTET	private function isActionDisabled(string $actionName): bool {
56437e13a7SLORTET		global $conf;
57437e13a7SLORTET
58437e13a7SLORTET		$disabled = explode(',', (string)($conf['disableactions'] ?? ''));
59437e13a7SLORTET		$disabled = array_map(static function ($value) {
60437e13a7SLORTET			return strtolower(trim((string)$value));
61437e13a7SLORTET		}, $disabled);
62437e13a7SLORTET		$actionName = strtolower(trim($actionName));
63437e13a7SLORTET		if ($actionName === '') return false;
64437e13a7SLORTET
65437e13a7SLORTET		return in_array($actionName, $disabled, true);
66437e13a7SLORTET	}
67437e13a7SLORTET
6819821f1cSLORTET	private function isLayoutIncludePage(): bool {
6919821f1cSLORTET		global $ID, $INFO;
7019821f1cSLORTET		// DokuWiki populates $INFO['id'] once for the originally requested page, but
7119821f1cSLORTET		// temporarily changes $ID while rendering layout includes (sidebar, footer, …)
7219821f1cSLORTET		// via tpl_include_page(). Comparing them detects any layout include without
7319821f1cSLORTET		// having to hardcode page names.
7419821f1cSLORTET		return isset($INFO['id']) && (string)$ID !== (string)$INFO['id'];
75b603bbe1SLORTET	}
76b603bbe1SLORTET
77b603bbe1SLORTET	public function setPageFavicon(Doku_Event $event): void {
78da933f89SLORTET		global $ACT, $ID;
79da933f89SLORTET
80da933f89SLORTET		if (!(bool)$this->getConf('show_as_favicon')) return;
81da933f89SLORTET		if ($ACT !== 'show') return;
82da933f89SLORTET
8319821f1cSLORTET		if ($this->isLayoutIncludePage()) return;
84da933f89SLORTET
85da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
86da933f89SLORTET		if (!$helper) return;
87da933f89SLORTET
88da933f89SLORTET		$namespace = getNS((string)$ID);
8919821f1cSLORTET		$pageID = noNS((string)$ID);
90*c8e99a27SLORTET		$size = $this->getIconSize();
91*c8e99a27SLORTET		$sizeMode = $size > 35 ? 'bigorsmall' : 'smallorbig';
92*c8e99a27SLORTET		$favicon = $helper->getPageIconUrl($namespace, $pageID, $sizeMode, ['w' => $size]);
93da933f89SLORTET		if (!$favicon) return;
94b09be489SLORTET		$favicon = html_entity_decode((string)$favicon, ENT_QUOTES | ENT_HTML5, 'UTF-8');
95da933f89SLORTET
96da933f89SLORTET		if (!isset($event->data['link']) || !is_array($event->data['link'])) {
97da933f89SLORTET			$event->data['link'] = [];
98da933f89SLORTET		}
99da933f89SLORTET
100da933f89SLORTET		$links = [];
101da933f89SLORTET		foreach ($event->data['link'] as $link) {
102da933f89SLORTET			if (!is_array($link)) {
103da933f89SLORTET				$links[] = $link;
104da933f89SLORTET				continue;
105da933f89SLORTET			}
106da933f89SLORTET
107da933f89SLORTET			$rels = $link['rel'] ?? '';
108da933f89SLORTET			if (!is_array($rels)) {
109da933f89SLORTET				$rels = preg_split('/\s+/', strtolower(trim((string)$rels))) ?: [];
110da933f89SLORTET			}
111da933f89SLORTET			$rels = array_filter(array_map('strtolower', (array)$rels));
112da933f89SLORTET			if (in_array('icon', $rels, true)) {
113da933f89SLORTET				continue;
114da933f89SLORTET			}
115da933f89SLORTET			$links[] = $link;
116da933f89SLORTET		}
117da933f89SLORTET
118da933f89SLORTET		$links[] = ['rel' => 'icon', 'href' => $favicon];
119b603bbe1SLORTET		$links[] = ['rel' => 'shortcut icon', 'href' => $favicon]; // Kept for legacy browser compatibility.
120da933f89SLORTET		$event->data['link'] = $links;
12119821f1cSLORTET
12219821f1cSLORTET		if (!isset($event->data['meta']) || !is_array($event->data['meta'])) {
12319821f1cSLORTET			$event->data['meta'] = [];
124da933f89SLORTET		}
12519821f1cSLORTET		$event->data['meta'][] = ['name' => 'pagesicon-favicon', 'content' => $favicon];
126b603bbe1SLORTET	}
127b603bbe1SLORTET
128b603bbe1SLORTET	public function addFaviconRuntimeScript(Doku_Event $event): void {
129b603bbe1SLORTET		global $ACT;
130b603bbe1SLORTET
131b603bbe1SLORTET		if (!(bool)$this->getConf('show_as_favicon')) return;
132b603bbe1SLORTET		if ($ACT !== 'show') return;
133b603bbe1SLORTET
134b603bbe1SLORTET		if (!isset($event->data['script']) || !is_array($event->data['script'])) {
135b603bbe1SLORTET			$event->data['script'] = [];
136b603bbe1SLORTET		}
137b603bbe1SLORTET
138b603bbe1SLORTET		$event->data['script'][] = [
139b603bbe1SLORTET			'type' => 'text/javascript',
140b603bbe1SLORTET			'src' => DOKU_BASE . 'lib/plugins/pagesicon/script/favicon-runtime.js',
141b603bbe1SLORTET			'_data' => 'pagesicon-favicon-runtime',
142b603bbe1SLORTET		];
143b603bbe1SLORTET	}
144b603bbe1SLORTET
14519821f1cSLORTET	public function addUploadFormScript(Doku_Event $event): void {
14619821f1cSLORTET		global $ACT;
14719821f1cSLORTET
14819821f1cSLORTET		if ($ACT !== 'pagesicon') return;
14919821f1cSLORTET
15019821f1cSLORTET		if (!isset($event->data['script']) || !is_array($event->data['script'])) {
15119821f1cSLORTET			$event->data['script'] = [];
152da933f89SLORTET		}
153da933f89SLORTET
15419821f1cSLORTET		$event->data['script'][] = [
15519821f1cSLORTET			'type' => 'text/javascript',
15619821f1cSLORTET			'src' => DOKU_BASE . 'lib/plugins/pagesicon/script/upload-form.js',
15719821f1cSLORTET			'_data' => 'pagesicon-upload-form',
15819821f1cSLORTET		];
15919821f1cSLORTET	}
160da933f89SLORTET
16119821f1cSLORTET	private function hasIconAlready(string $html): bool {
16219821f1cSLORTET		return strpos($html, 'class="pagesicon-injected"') !== false;
163da933f89SLORTET	}
164da933f89SLORTET
165b603bbe1SLORTET	private function canUploadToTarget(string $targetPage): bool {
166da933f89SLORTET		if ($targetPage === '') return false;
167da933f89SLORTET		return auth_quickaclcheck($targetPage) >= AUTH_UPLOAD;
168da933f89SLORTET	}
169da933f89SLORTET
170b603bbe1SLORTET	private function getDefaultTarget(): string {
171da933f89SLORTET		global $ID;
172da933f89SLORTET		return cleanID((string)$ID);
173da933f89SLORTET	}
174da933f89SLORTET
175b603bbe1SLORTET	private function getDefaultVariant(): string {
176da933f89SLORTET		global $INPUT;
177da933f89SLORTET		$defaultVariant = strtolower($INPUT->str('icon_variant'));
178da933f89SLORTET		if (!in_array($defaultVariant, ['big', 'small'], true)) {
179da933f89SLORTET			$defaultVariant = 'big';
180da933f89SLORTET		}
181da933f89SLORTET		return $defaultVariant;
182da933f89SLORTET	}
183da933f89SLORTET
184b603bbe1SLORTET	private function getPostedBaseName(array $choices): string {
185da933f89SLORTET		global $INPUT;
186b603bbe1SLORTET		/** @var helper_plugin_pagesicon|null $helper */
187b603bbe1SLORTET		$helper = plugin_load('helper', 'pagesicon');
188b603bbe1SLORTET		$selected = $helper ? $helper->normalizeIconBaseName($INPUT->post->str('icon_filename')) : '';
189da933f89SLORTET		if ($selected !== '' && isset($choices[$selected])) return $selected;
190da933f89SLORTET		return (string)array_key_first($choices);
191da933f89SLORTET	}
192da933f89SLORTET
193b603bbe1SLORTET	private function getMediaManagerUrl(string $targetPage): string {
194da933f89SLORTET		$namespace = getNS($targetPage);
195b603bbe1SLORTET		return DOKU_MEDIAMANAGER_URL_BASE . '?ns=' . rawurlencode($namespace);
196da933f89SLORTET	}
197da933f89SLORTET
198b603bbe1SLORTET	private function renderCurrentIconPreview(string $mediaID, string $defaultTarget, string $actionPage, int $previewSize): void {
199b603bbe1SLORTET		echo '<a href="' . hsc($this->getMediaManagerUrl($defaultTarget)) . '" target="_blank" title="' . hsc($this->getLang('open_media_manager')) . '">';
200b603bbe1SLORTET		echo '<img src="' . ml($mediaID, ['w' => $previewSize]) . '" alt="" width="' . $previewSize . '" style="display:block;margin:6px 0;" />';
201b603bbe1SLORTET		echo '</a>';
202b603bbe1SLORTET		echo '<small>' . hsc(noNS($mediaID)) . '</small>';
203b603bbe1SLORTET		echo '<form action="' . wl($actionPage) . '" method="post" style="margin-top:6px;">';
204b603bbe1SLORTET		formSecurityToken();
205b603bbe1SLORTET		echo '<input type="hidden" name="do" value="pagesicon" />';
206b603bbe1SLORTET		echo '<input type="hidden" name="media_id" value="' . hsc($mediaID) . '" />';
207b603bbe1SLORTET		echo '<input type="hidden" name="pagesicon_delete_submit" value="1" />';
208b603bbe1SLORTET		echo '<button type="submit" class="button">' . hsc($this->getLang('delete_icon')) . '</button>';
209b603bbe1SLORTET		echo '</form>';
210da933f89SLORTET	}
211da933f89SLORTET
212b603bbe1SLORTET	private function handleDeletePost(): void {
213da933f89SLORTET		global $INPUT, $ID;
214da933f89SLORTET
215da933f89SLORTET		if (!$INPUT->post->has('pagesicon_delete_submit')) return;
216da933f89SLORTET		if (!checkSecurityToken()) return;
217da933f89SLORTET
218da933f89SLORTET		$targetPage = cleanID((string)$ID);
219da933f89SLORTET		$mediaID = cleanID($INPUT->post->str('media_id'));
220da933f89SLORTET
221da933f89SLORTET		if ($targetPage === '' || $mediaID === '') {
222da933f89SLORTET			msg($this->getLang('error_delete_invalid'), -1);
223da933f89SLORTET			return;
224da933f89SLORTET		}
225da933f89SLORTET		if (!$this->canUploadToTarget($targetPage)) {
226da933f89SLORTET			msg($this->getLang('error_no_upload_permission'), -1);
227da933f89SLORTET			return;
228da933f89SLORTET		}
229da933f89SLORTET		$namespace = getNS($targetPage);
230da933f89SLORTET		$pageID = noNS($targetPage);
231da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
232b603bbe1SLORTET		$currentBig = ($helper && method_exists($helper, 'getPageIconId')) ? (string)$helper->getPageIconId($namespace, $pageID, 'big') : '';
233b603bbe1SLORTET		$currentSmall = ($helper && method_exists($helper, 'getPageIconId')) ? (string)$helper->getPageIconId($namespace, $pageID, 'small') : '';
234da933f89SLORTET		$allowed = array_values(array_filter(array_unique([$currentBig, $currentSmall])));
235da933f89SLORTET		if (!$allowed || !in_array($mediaID, $allowed, true)) {
236da933f89SLORTET			msg($this->getLang('error_delete_invalid'), -1);
237da933f89SLORTET			return;
238da933f89SLORTET		}
239da933f89SLORTET
240da933f89SLORTET		$file = mediaFN($mediaID);
241da933f89SLORTET		if (!@file_exists($file)) {
242da933f89SLORTET			msg($this->getLang('error_delete_not_found'), -1);
243da933f89SLORTET			return;
244da933f89SLORTET		}
245da933f89SLORTET		if (!@unlink($file)) {
246da933f89SLORTET			msg($this->getLang('error_delete_failed'), -1);
247da933f89SLORTET			return;
248da933f89SLORTET		}
249da933f89SLORTET
250b603bbe1SLORTET		if ($helper) {
251b603bbe1SLORTET			$helper->notifyIconUpdated($targetPage, 'delete', $mediaID);
252b603bbe1SLORTET		}
253da933f89SLORTET		msg(sprintf($this->getLang('delete_success'), hsc($mediaID)), 1);
254da933f89SLORTET	}
255da933f89SLORTET
256b603bbe1SLORTET	private function handleUploadPost(): void {
257b603bbe1SLORTET		global $INPUT, $ID, $conf;
258da933f89SLORTET
259da933f89SLORTET		if (!$INPUT->post->has('pagesicon_upload_submit')) return;
260da933f89SLORTET		if (!checkSecurityToken()) return;
261da933f89SLORTET
262da933f89SLORTET		$targetPage = cleanID((string)$ID);
263da933f89SLORTET		if (!$this->canUploadToTarget($targetPage)) {
264da933f89SLORTET			msg($this->getLang('error_no_upload_permission'), -1);
265da933f89SLORTET			return;
266da933f89SLORTET		}
267da933f89SLORTET
268da933f89SLORTET		$variant = strtolower($INPUT->post->str('icon_variant'));
269da933f89SLORTET		if (!in_array($variant, ['big', 'small'], true)) {
270da933f89SLORTET			$variant = 'big';
271da933f89SLORTET		}
272da933f89SLORTET
273da933f89SLORTET		if (!isset($_FILES['pagesicon_file']) || !is_array($_FILES['pagesicon_file'])) {
274da933f89SLORTET			msg($this->getLang('error_missing_file'), -1);
275da933f89SLORTET			return;
276da933f89SLORTET		}
277da933f89SLORTET
278da933f89SLORTET		$upload = $_FILES['pagesicon_file'];
279da933f89SLORTET		if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
280da933f89SLORTET			msg($this->getLang('error_upload_failed') . ' (' . (int)($upload['error'] ?? -1) . ')', -1);
281da933f89SLORTET			return;
282da933f89SLORTET		}
283da933f89SLORTET
284da933f89SLORTET		$originalName = (string)($upload['name'] ?? '');
285da933f89SLORTET		$tmpName = (string)($upload['tmp_name'] ?? '');
286da933f89SLORTET		if ($tmpName === '' || !is_uploaded_file($tmpName)) {
287da933f89SLORTET			msg($this->getLang('error_upload_failed'), -1);
288da933f89SLORTET			return;
289da933f89SLORTET		}
290da933f89SLORTET
291da933f89SLORTET		$ext = strtolower((string)pathinfo($originalName, PATHINFO_EXTENSION));
292da933f89SLORTET		if ($ext === '') {
293da933f89SLORTET			msg($this->getLang('error_extension_missing'), -1);
294da933f89SLORTET			return;
295da933f89SLORTET		}
296da933f89SLORTET
297b603bbe1SLORTET		$helper = plugin_load('helper', 'pagesicon');
298b603bbe1SLORTET		$allowed = ($helper && method_exists($helper, 'getConfiguredExtensions'))
299b603bbe1SLORTET			? $helper->getConfiguredExtensions()
300b603bbe1SLORTET			: [];
301da933f89SLORTET		if (!in_array($ext, $allowed, true)) {
302da933f89SLORTET			msg(sprintf($this->getLang('error_extension_not_allowed'), hsc($ext), hsc(implode(', ', $allowed))), -1);
303da933f89SLORTET			return;
304da933f89SLORTET		}
305da933f89SLORTET
306b603bbe1SLORTET		$choices = ($helper && method_exists($helper, 'getUploadNameChoices'))
307b603bbe1SLORTET			? $helper->getUploadNameChoices($targetPage, $variant)
308b603bbe1SLORTET			: [];
309da933f89SLORTET		$base = $this->getPostedBaseName($choices);
310da933f89SLORTET		$namespace = getNS($targetPage);
311da933f89SLORTET		$mediaBase = $namespace !== '' ? ($namespace . ':' . $base) : $base;
312da933f89SLORTET		$mediaID = cleanID($mediaBase . '.' . $ext);
313da933f89SLORTET		$targetFile = mediaFN($mediaID);
314da933f89SLORTET
315da933f89SLORTET		io_makeFileDir($targetFile);
316da933f89SLORTET		if (!@is_dir(dirname($targetFile))) {
317da933f89SLORTET			msg($this->getLang('error_write_dir'), -1);
318da933f89SLORTET			return;
319da933f89SLORTET		}
320da933f89SLORTET
321da933f89SLORTET		$moved = @move_uploaded_file($tmpName, $targetFile);
322da933f89SLORTET		if (!$moved) {
323da933f89SLORTET			$moved = @copy($tmpName, $targetFile);
324da933f89SLORTET		}
325da933f89SLORTET		if (!$moved) {
326da933f89SLORTET			msg($this->getLang('error_write_file'), -1);
327da933f89SLORTET			return;
328da933f89SLORTET		}
329da933f89SLORTET
330b603bbe1SLORTET		@chmod($targetFile, $conf['fmode']);
331b603bbe1SLORTET		if ($helper) {
332b603bbe1SLORTET			$helper->notifyIconUpdated($targetPage, 'upload', $mediaID);
333b603bbe1SLORTET		}
334da933f89SLORTET		msg(sprintf($this->getLang('upload_success'), hsc($mediaID)), 1);
335da933f89SLORTET	}
336da933f89SLORTET
337b603bbe1SLORTET	private function renderUploadForm(): void {
338da933f89SLORTET		global $ID, $INPUT;
339da933f89SLORTET
340da933f89SLORTET		$defaultTarget = $this->getDefaultTarget();
341da933f89SLORTET		$defaultVariant = $this->getDefaultVariant();
342b603bbe1SLORTET		$helper = plugin_load('helper', 'pagesicon');
343b603bbe1SLORTET		$allowed = ($helper && method_exists($helper, 'getConfiguredExtensions'))
344b603bbe1SLORTET			? implode(', ', $helper->getConfiguredExtensions())
345b603bbe1SLORTET			: '';
346b603bbe1SLORTET		$currentChoices = ($helper && method_exists($helper, 'getUploadNameChoices'))
347b603bbe1SLORTET			? $helper->getUploadNameChoices($defaultTarget, $defaultVariant)
348b603bbe1SLORTET			: [];
349b603bbe1SLORTET		$selectedBase = $helper ? $helper->normalizeIconBaseName($INPUT->str('icon_filename')) : '';
350da933f89SLORTET		if (!isset($currentChoices[$selectedBase])) {
351da933f89SLORTET			$selectedBase = (string)array_key_first($currentChoices);
352da933f89SLORTET		}
353da933f89SLORTET		$filenameHelp = hsc($this->getLang('icon_filename_help'));
354da933f89SLORTET		$actionPage = $defaultTarget !== '' ? $defaultTarget : cleanID((string)$ID);
355da933f89SLORTET		$namespace = getNS($defaultTarget);
356da933f89SLORTET		$pageID = noNS($defaultTarget);
357b603bbe1SLORTET		$previewSize = $this->getIconSize();
358b603bbe1SLORTET		$currentBig = ($helper && method_exists($helper, 'getPageIconId')) ? $helper->getPageIconId($namespace, $pageID, 'big') : false;
359b603bbe1SLORTET		$currentSmall = ($helper && method_exists($helper, 'getPageIconId')) ? $helper->getPageIconId($namespace, $pageID, 'small') : false;
360da933f89SLORTET
361da933f89SLORTET		echo '<h1>' . hsc($this->getLang('menu')) . '</h1>';
362da933f89SLORTET		echo '<p>' . hsc($this->getLang('intro')) . '</p>';
363da933f89SLORTET		echo '<p><small>' . hsc(sprintf($this->getLang('allowed_extensions'), $allowed)) . '</small></p>';
364da933f89SLORTET		echo '<div class="pagesicon-current-preview" style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;margin:10px 0 16px;">';
365da933f89SLORTET		echo '<div class="pagesicon-current-item">';
366da933f89SLORTET		echo '<strong>' . hsc($this->getLang('current_big_icon')) . '</strong><br />';
367da933f89SLORTET		if ($currentBig) {
368b603bbe1SLORTET			$this->renderCurrentIconPreview($currentBig, $defaultTarget, $actionPage, $previewSize);
369da933f89SLORTET		} else {
370da933f89SLORTET			echo '<small>' . hsc($this->getLang('current_icon_none')) . '</small>';
371da933f89SLORTET		}
372da933f89SLORTET		echo '</div>';
373da933f89SLORTET		echo '<div class="pagesicon-current-item">';
374da933f89SLORTET		echo '<strong>' . hsc($this->getLang('current_small_icon')) . '</strong><br />';
375da933f89SLORTET		if ($currentSmall) {
376b603bbe1SLORTET			$this->renderCurrentIconPreview($currentSmall, $defaultTarget, $actionPage, $previewSize);
377da933f89SLORTET		} else {
378da933f89SLORTET			echo '<small>' . hsc($this->getLang('current_icon_none')) . '</small>';
379da933f89SLORTET		}
380da933f89SLORTET		echo '</div>';
381da933f89SLORTET		echo '</div>';
382da933f89SLORTET
383b603bbe1SLORTET		echo '<form action="' . wl($actionPage) . '" method="post" enctype="multipart/form-data"'
384b603bbe1SLORTET			. ' class="pagesicon-upload-form"'
385b603bbe1SLORTET			. ' data-page-name="' . hsc(noNS($defaultTarget)) . '"'
386b603bbe1SLORTET			. ' data-big-templates="' . hsc(json_encode($helper ? $helper->getVariantTemplates('big') : [])) . '"'
387b603bbe1SLORTET			. ' data-small-templates="' . hsc(json_encode($helper ? $helper->getVariantTemplates('small') : [])) . '">';
388da933f89SLORTET		formSecurityToken();
389da933f89SLORTET		echo '<input type="hidden" name="do" value="pagesicon" />';
390da933f89SLORTET		echo '<input type="hidden" name="pagesicon_upload_submit" value="1" />';
391da933f89SLORTET
392da933f89SLORTET		echo '<div class="table"><table class="inline">';
393da933f89SLORTET		echo '<tr>';
394da933f89SLORTET		echo '<td class="label"><label for="pagesicon_icon_variant">' . hsc($this->getLang('icon_variant')) . '</label></td>';
395da933f89SLORTET		echo '<td>';
396da933f89SLORTET		echo '<select id="pagesicon_icon_variant" name="icon_variant" class="edit">';
397da933f89SLORTET		echo '<option value="big"' . ($defaultVariant === 'big' ? ' selected="selected"' : '') . '>' . hsc($this->getLang('icon_variant_big')) . '</option>';
398da933f89SLORTET		echo '<option value="small"' . ($defaultVariant === 'small' ? ' selected="selected"' : '') . '>' . hsc($this->getLang('icon_variant_small')) . '</option>';
399da933f89SLORTET		echo '</select>';
400da933f89SLORTET		echo '</td>';
401da933f89SLORTET		echo '</tr>';
402da933f89SLORTET
403da933f89SLORTET		echo '<tr>';
404da933f89SLORTET		echo '<td class="label"><label for="pagesicon_file">' . hsc($this->getLang('file')) . '</label></td>';
405da933f89SLORTET		echo '<td><input type="file" id="pagesicon_file" name="pagesicon_file" class="edit" required /></td>';
406da933f89SLORTET		echo '</tr>';
407da933f89SLORTET
408da933f89SLORTET		echo '<tr>';
409da933f89SLORTET		echo '<td class="label"><label for="pagesicon_icon_filename">' . hsc($this->getLang('icon_filename')) . '</label></td>';
410da933f89SLORTET		echo '<td>';
411b603bbe1SLORTET		if ($currentChoices) {
412da933f89SLORTET			echo '<select id="pagesicon_icon_filename" name="icon_filename" class="edit">';
413da933f89SLORTET			foreach ($currentChoices as $value => $label) {
414da933f89SLORTET				$selected = $value === $selectedBase ? ' selected="selected"' : '';
415da933f89SLORTET				echo '<option value="' . hsc($value) . '"' . $selected . '>' . hsc($label) . '</option>';
416da933f89SLORTET			}
417da933f89SLORTET			echo '</select>';
418da933f89SLORTET			echo '<br /><small>' . $filenameHelp . '</small>';
419b603bbe1SLORTET		} else {
420b603bbe1SLORTET			echo '<span class="error">' . hsc($this->getLang('error_no_filename_choices')) . '</span>';
421b603bbe1SLORTET		}
422da933f89SLORTET		echo '</td>';
423da933f89SLORTET		echo '</tr>';
424da933f89SLORTET		echo '</table></div>';
425da933f89SLORTET
426da933f89SLORTET		echo '<p><button type="submit" class="button">' . hsc($this->getLang('upload_button')) . '</button></p>';
427da933f89SLORTET		echo '</form>';
428da933f89SLORTET	}
429da933f89SLORTET
43019821f1cSLORTET	public function displayPageIcon(Doku_Event &$event, $param): void {
431da933f89SLORTET		global $ACT, $ID;
432da933f89SLORTET
433da933f89SLORTET		if($ACT !== 'show') return;
434da933f89SLORTET		if(!(bool)$this->getConf('show_on_top')) return;
435da933f89SLORTET
43619821f1cSLORTET		if($this->isLayoutIncludePage()) return;
437da933f89SLORTET
438da933f89SLORTET		$namespace = getNS($ID);
439da933f89SLORTET		$pageID = noNS((string)$ID);
440da933f89SLORTET		/** @var helper_plugin_pagesicon|null $helper */
441da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
442da933f89SLORTET		if(!$helper) return;
443da933f89SLORTET		$sizeMode = $this->getIconSize() > 35 ? 'bigorsmall' : 'smallorbig';
444b603bbe1SLORTET		$logoMediaID = $helper->getPageIconId($namespace, $pageID, $sizeMode);
445da933f89SLORTET		if(!$logoMediaID) return;
44619821f1cSLORTET		if($this->hasIconAlready($event->data)) return;
447da933f89SLORTET
448da933f89SLORTET		$size = $this->getIconSize();
449b603bbe1SLORTET		$src = $helper->getPageIconUrl($namespace, $pageID, $sizeMode, ['w' => $size]);
450da933f89SLORTET		if(!$src) return;
451da933f89SLORTET		$iconHtml = '<img src="' . $src . '" class="media pagesicon-image" loading="lazy" alt="" width="' . $size . '" />';
452da933f89SLORTET
453da933f89SLORTET		$inlineIcon = '<span class="pagesicon-injected pagesicon-injected-inline">' . $iconHtml . '</span> ';
454da933f89SLORTET		$updated = preg_replace('/<h1\b([^>]*)>/i', '<h1$1>' . $inlineIcon, $event->data, 1, $count);
455da933f89SLORTET		if ($count > 0 && $updated !== null) {
456da933f89SLORTET			$event->data = $updated;
457da933f89SLORTET			return;
458da933f89SLORTET		}
459da933f89SLORTET
460da933f89SLORTET		// Fallback: no H1 found, keep old behavior
461da933f89SLORTET		$event->data = '<div class="pagesicon-injected">' . $iconHtml . '</div>' . "\n" . $event->data;
462da933f89SLORTET	}
463da933f89SLORTET
464*c8e99a27SLORTET	private static array $linkIconCache = [];
465*c8e99a27SLORTET
466*c8e99a27SLORTET	private function getLinkIconUrl(object $helper, string $pageId): ?string {
467*c8e99a27SLORTET		if (!array_key_exists($pageId, self::$linkIconCache)) {
468*c8e99a27SLORTET			$url = $helper->getPageIconUrl(getNS($pageId), noNS($pageId), 'smallorbig', ['w' => 16]);
469*c8e99a27SLORTET			self::$linkIconCache[$pageId] = $url ?: null;
470*c8e99a27SLORTET		}
471*c8e99a27SLORTET		return self::$linkIconCache[$pageId];
472*c8e99a27SLORTET	}
473*c8e99a27SLORTET
474*c8e99a27SLORTET	public function injectLinkIcons(Doku_Event $event): void {
475*c8e99a27SLORTET		if ($event->data[0] !== 'xhtml') return;
476*c8e99a27SLORTET
477*c8e99a27SLORTET		$conf = $this->getConf('link_icons');
478*c8e99a27SLORTET		if ($conf === 'none') return;
479*c8e99a27SLORTET
480*c8e99a27SLORTET		$helper = plugin_load('helper', 'pagesicon');
481*c8e99a27SLORTET		if (!$helper) return;
482*c8e99a27SLORTET
483*c8e99a27SLORTET		$event->data[1] = preg_replace_callback(
484*c8e99a27SLORTET			'~(<a\b[^>]*\bclass="[^"]*\bwikilink([12])[^"]*"[^>]*\btitle="([^"]+)"[^>]*>)~',
485*c8e99a27SLORTET			function ($m) use ($conf, $helper) {
486*c8e99a27SLORTET				if ($m[2] === '2' && $conf !== 'all') return $m[1];
487*c8e99a27SLORTET				$pageId = html_entity_decode($m[3], ENT_QUOTES | ENT_HTML5, 'UTF-8');
488*c8e99a27SLORTET				$iconUrl = $this->getLinkIconUrl($helper, $pageId);
489*c8e99a27SLORTET				if (!$iconUrl) return $m[1];
490*c8e99a27SLORTET				return $m[1] . '<img src="' . $iconUrl . '" class="pagesicon-link" alt="" width="16" height="16" loading="lazy">';
491*c8e99a27SLORTET			},
492*c8e99a27SLORTET			(string)$event->data[1]
493*c8e99a27SLORTET		);
494*c8e99a27SLORTET	}
495*c8e99a27SLORTET
496b603bbe1SLORTET	public function handleAction(Doku_Event $event): void {
497da933f89SLORTET		if ($event->data !== 'pagesicon') return;
498da933f89SLORTET		$event->preventDefault();
499da933f89SLORTET	}
500da933f89SLORTET
501b603bbe1SLORTET	public function renderAction(Doku_Event $event): void {
502da933f89SLORTET		global $ACT;
503da933f89SLORTET		if ($ACT !== 'pagesicon') return;
504da933f89SLORTET
505da933f89SLORTET		$this->handleDeletePost();
506da933f89SLORTET		$this->handleUploadPost();
507da933f89SLORTET		$this->renderUploadForm();
508da933f89SLORTET
509da933f89SLORTET		$event->preventDefault();
510da933f89SLORTET		$event->stopPropagation();
511da933f89SLORTET	}
512da933f89SLORTET}
513