xref: /plugin/pagesicon/action.php (revision da933f899c3fc9c630c3a0106c319fb606ed9385)
1*da933f89SLORTET<?php
2*da933f89SLORTETif(!defined('DOKU_INC')) die();
3*da933f89SLORTET
4*da933f89SLORTETclass action_plugin_pagesicon extends DokuWiki_Action_Plugin {
5*da933f89SLORTET
6*da933f89SLORTET	public function register(Doku_Event_Handler $controller) {
7*da933f89SLORTET		$controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, '_displaypageicon');
8*da933f89SLORTET		$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'setPageFavicon');
9*da933f89SLORTET		$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleAction');
10*da933f89SLORTET		$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'renderAction');
11*da933f89SLORTET		$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addPageAction');
12*da933f89SLORTET	}
13*da933f89SLORTET
14*da933f89SLORTET	public function addPageAction(Doku_Event $event): void
15*da933f89SLORTET	{
16*da933f89SLORTET		global $ID;
17*da933f89SLORTET
18*da933f89SLORTET		if (($event->data['view'] ?? '') !== 'page') return;
19*da933f89SLORTET		if (auth_quickaclcheck((string)$ID) < AUTH_UPLOAD) return;
20*da933f89SLORTET
21*da933f89SLORTET		foreach (($event->data['items'] ?? []) as $item) {
22*da933f89SLORTET			if ($item instanceof \dokuwiki\Menu\Item\AbstractItem && $item->getType() === 'pagesicon') {
23*da933f89SLORTET				return;
24*da933f89SLORTET			}
25*da933f89SLORTET		}
26*da933f89SLORTET
27*da933f89SLORTET		$label = (string)$this->getLang('page_action');
28*da933f89SLORTET		if ($label === '') $label = 'Gerer l\'icone';
29*da933f89SLORTET		$title = (string)$this->getLang('page_action_title');
30*da933f89SLORTET		if ($title === '') $title = $label;
31*da933f89SLORTET		$targetPage = cleanID((string)$ID);
32*da933f89SLORTET
33*da933f89SLORTET		$event->data['items'][] = new class($targetPage, $label, $title) extends \dokuwiki\Menu\Item\AbstractItem {
34*da933f89SLORTET			public function __construct(string $targetPage, string $label, string $title)
35*da933f89SLORTET			{
36*da933f89SLORTET				parent::__construct();
37*da933f89SLORTET				$this->type = 'pagesicon';
38*da933f89SLORTET				$this->id = $targetPage;
39*da933f89SLORTET				$this->params = [
40*da933f89SLORTET					'do' => 'pagesicon',
41*da933f89SLORTET				];
42*da933f89SLORTET				$this->label = $label;
43*da933f89SLORTET				$this->title = $title;
44*da933f89SLORTET				$this->svg = DOKU_INC . 'lib/images/menu/folder-multiple-image.svg';
45*da933f89SLORTET			}
46*da933f89SLORTET		};
47*da933f89SLORTET	}
48*da933f89SLORTET
49*da933f89SLORTET	private function getIconSize(): int {
50*da933f89SLORTET		$size = (int)$this->getConf('icon_size');
51*da933f89SLORTET		if($size < 8) return 55;
52*da933f89SLORTET		if($size > 512) return 512;
53*da933f89SLORTET		return $size;
54*da933f89SLORTET	}
55*da933f89SLORTET
56*da933f89SLORTET	public function setPageFavicon(Doku_Event $event): void
57*da933f89SLORTET	{
58*da933f89SLORTET		global $ACT, $ID;
59*da933f89SLORTET
60*da933f89SLORTET		if (!(bool)$this->getConf('show_as_favicon')) return;
61*da933f89SLORTET		if ($ACT !== 'show') return;
62*da933f89SLORTET
63*da933f89SLORTET		$pageID = noNS((string)$ID);
64*da933f89SLORTET		if ($pageID === 'sidebar' || $pageID === 'footer') return;
65*da933f89SLORTET
66*da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
67*da933f89SLORTET		if (!$helper) return;
68*da933f89SLORTET
69*da933f89SLORTET		$namespace = getNS((string)$ID);
70*da933f89SLORTET		$iconMediaID = $helper->getPageImage($namespace, $pageID, 'smallorbig');
71*da933f89SLORTET		if (!$iconMediaID) return;
72*da933f89SLORTET
73*da933f89SLORTET		$favicon = html_entity_decode((string)ml($iconMediaID, ['w' => 32]), ENT_QUOTES | ENT_HTML5, 'UTF-8');
74*da933f89SLORTET		$favicon = $this->addVersionToUrl($favicon, $this->getMediaVersionStamp($iconMediaID), false);
75*da933f89SLORTET		if (!$favicon) return;
76*da933f89SLORTET
77*da933f89SLORTET		if (!isset($event->data['link']) || !is_array($event->data['link'])) {
78*da933f89SLORTET			$event->data['link'] = [];
79*da933f89SLORTET		}
80*da933f89SLORTET
81*da933f89SLORTET		$links = [];
82*da933f89SLORTET		foreach ($event->data['link'] as $link) {
83*da933f89SLORTET			if (!is_array($link)) {
84*da933f89SLORTET				$links[] = $link;
85*da933f89SLORTET				continue;
86*da933f89SLORTET			}
87*da933f89SLORTET
88*da933f89SLORTET			$rels = $link['rel'] ?? '';
89*da933f89SLORTET			if (!is_array($rels)) {
90*da933f89SLORTET				$rels = preg_split('/\s+/', strtolower(trim((string)$rels))) ?: [];
91*da933f89SLORTET			}
92*da933f89SLORTET			$rels = array_filter(array_map('strtolower', (array)$rels));
93*da933f89SLORTET			if (in_array('icon', $rels, true)) {
94*da933f89SLORTET				continue;
95*da933f89SLORTET			}
96*da933f89SLORTET			$links[] = $link;
97*da933f89SLORTET		}
98*da933f89SLORTET
99*da933f89SLORTET		$links[] = ['rel' => 'icon', 'href' => $favicon];
100*da933f89SLORTET		$links[] = ['rel' => 'shortcut icon', 'href' => $favicon];
101*da933f89SLORTET		$event->data['link'] = $links;
102*da933f89SLORTET	}
103*da933f89SLORTET
104*da933f89SLORTET	private function hasIconAlready(string $html, string $mediaID): bool {
105*da933f89SLORTET		return strpos($html, 'class="pagesicon-injected"') !== false;
106*da933f89SLORTET	}
107*da933f89SLORTET
108*da933f89SLORTET	private function getMediaVersionStamp(string $mediaID): string
109*da933f89SLORTET	{
110*da933f89SLORTET		$file = mediaFN($mediaID);
111*da933f89SLORTET		if (!@file_exists($file)) return '';
112*da933f89SLORTET		$mtime = @filemtime($file);
113*da933f89SLORTET		if (!$mtime) return '';
114*da933f89SLORTET		return (string)$mtime;
115*da933f89SLORTET	}
116*da933f89SLORTET
117*da933f89SLORTET	private function addVersionToUrl(string $url, string $version, bool $htmlEncodedAmp = true): string
118*da933f89SLORTET	{
119*da933f89SLORTET		if ($url === '' || $version === '') return $url;
120*da933f89SLORTET		$sep = strpos($url, '?') === false ? '?' : ($htmlEncodedAmp ? '&amp;' : '&');
121*da933f89SLORTET		return $url . $sep . 'pi_ts=' . rawurlencode($version);
122*da933f89SLORTET	}
123*da933f89SLORTET
124*da933f89SLORTET	private function injectFaviconRuntimeScript(string &$html, string $faviconHref): void
125*da933f89SLORTET	{
126*da933f89SLORTET		if ($faviconHref === '') return;
127*da933f89SLORTET
128*da933f89SLORTET		$href = json_encode($faviconHref);
129*da933f89SLORTET		$script = '<script>(function(){'
130*da933f89SLORTET			. 'var href=' . $href . ';'
131*da933f89SLORTET			. 'if(!href||!document.head)return;'
132*da933f89SLORTET			. 'var links=document.head.querySelectorAll(\'link[rel*="icon"]\');'
133*da933f89SLORTET			. 'for(var i=0;i<links.length;i++){links[i].parentNode.removeChild(links[i]);}'
134*da933f89SLORTET			. 'var icon=document.createElement("link");icon.rel="icon";icon.href=href;document.head.appendChild(icon);'
135*da933f89SLORTET			. 'var shortcut=document.createElement("link");shortcut.rel="shortcut icon";shortcut.href=href;document.head.appendChild(shortcut);'
136*da933f89SLORTET			. '})();</script>';
137*da933f89SLORTET
138*da933f89SLORTET		$html = $script . $html;
139*da933f89SLORTET	}
140*da933f89SLORTET
141*da933f89SLORTET	private function getAllowedExtensions(): array
142*da933f89SLORTET	{
143*da933f89SLORTET		$raw = trim((string)$this->getConf('extensions'));
144*da933f89SLORTET		if ($raw === '') return ['svg', 'png', 'jpg', 'jpeg'];
145*da933f89SLORTET
146*da933f89SLORTET		$extensions = array_values(array_unique(array_filter(array_map(function ($ext) {
147*da933f89SLORTET			return strtolower(ltrim(trim((string)$ext), '.'));
148*da933f89SLORTET		}, explode(';', $raw)))));
149*da933f89SLORTET
150*da933f89SLORTET		return $extensions ?: ['svg', 'png', 'jpg', 'jpeg'];
151*da933f89SLORTET	}
152*da933f89SLORTET
153*da933f89SLORTET	private function canUploadToTarget(string $targetPage): bool
154*da933f89SLORTET	{
155*da933f89SLORTET		if ($targetPage === '') return false;
156*da933f89SLORTET		return auth_quickaclcheck($targetPage) >= AUTH_UPLOAD;
157*da933f89SLORTET	}
158*da933f89SLORTET
159*da933f89SLORTET	private function getDefaultTarget(): string
160*da933f89SLORTET	{
161*da933f89SLORTET		global $ID;
162*da933f89SLORTET		return cleanID((string)$ID);
163*da933f89SLORTET	}
164*da933f89SLORTET
165*da933f89SLORTET	private function getDefaultVariant(): string
166*da933f89SLORTET	{
167*da933f89SLORTET		global $INPUT;
168*da933f89SLORTET		$defaultVariant = strtolower($INPUT->str('icon_variant'));
169*da933f89SLORTET		if (!in_array($defaultVariant, ['big', 'small'], true)) {
170*da933f89SLORTET			$defaultVariant = 'big';
171*da933f89SLORTET		}
172*da933f89SLORTET		return $defaultVariant;
173*da933f89SLORTET	}
174*da933f89SLORTET
175*da933f89SLORTET	private function getVariantTemplates(string $variant): array
176*da933f89SLORTET	{
177*da933f89SLORTET		$raw = $variant === 'small'
178*da933f89SLORTET			? (string)$this->getConf('icon_thumbnail_name')
179*da933f89SLORTET			: (string)$this->getConf('icon_name');
180*da933f89SLORTET
181*da933f89SLORTET		$templates = array_values(array_unique(array_filter(array_map('trim', explode(';', $raw)))));
182*da933f89SLORTET		if (!$templates) {
183*da933f89SLORTET			return [$variant === 'small' ? 'thumbnail' : 'icon'];
184*da933f89SLORTET		}
185*da933f89SLORTET		return $templates;
186*da933f89SLORTET	}
187*da933f89SLORTET
188*da933f89SLORTET	private function normalizeBaseName(string $name): string
189*da933f89SLORTET	{
190*da933f89SLORTET		$name = trim($name);
191*da933f89SLORTET		if ($name === '') return '';
192*da933f89SLORTET		$name = noNS($name);
193*da933f89SLORTET		$name = preg_replace('/\.[a-z0-9]+$/i', '', $name);
194*da933f89SLORTET		$name = cleanID($name);
195*da933f89SLORTET		return str_replace(':', '', $name);
196*da933f89SLORTET	}
197*da933f89SLORTET
198*da933f89SLORTET	private function getUploadNameChoices(string $targetPage, string $variant): array
199*da933f89SLORTET	{
200*da933f89SLORTET		$pageID = noNS($targetPage);
201*da933f89SLORTET		$choices = [];
202*da933f89SLORTET
203*da933f89SLORTET		foreach ($this->getVariantTemplates($variant) as $tpl) {
204*da933f89SLORTET			$resolved = str_replace('~pagename~', $pageID, $tpl);
205*da933f89SLORTET			$base = $this->normalizeBaseName($resolved);
206*da933f89SLORTET			if ($base === '') continue;
207*da933f89SLORTET			$choices[$base] = $base . '.ext';
208*da933f89SLORTET		}
209*da933f89SLORTET
210*da933f89SLORTET		if (!$choices) {
211*da933f89SLORTET			$fallback = $variant === 'small' ? 'thumbnail' : 'icon';
212*da933f89SLORTET			$choices[$fallback] = $fallback . '.ext';
213*da933f89SLORTET		}
214*da933f89SLORTET
215*da933f89SLORTET		return $choices;
216*da933f89SLORTET	}
217*da933f89SLORTET
218*da933f89SLORTET	private function getPostedBaseName(array $choices): string
219*da933f89SLORTET	{
220*da933f89SLORTET		global $INPUT;
221*da933f89SLORTET		$selected = $this->normalizeBaseName($INPUT->post->str('icon_filename'));
222*da933f89SLORTET		if ($selected !== '' && isset($choices[$selected])) return $selected;
223*da933f89SLORTET		return (string)array_key_first($choices);
224*da933f89SLORTET	}
225*da933f89SLORTET
226*da933f89SLORTET	private function getMediaManagerUrl(string $targetPage): string
227*da933f89SLORTET	{
228*da933f89SLORTET		$namespace = getNS($targetPage);
229*da933f89SLORTET		return DOKU_BASE . 'lib/exe/mediamanager.php?ns=' . rawurlencode($namespace);
230*da933f89SLORTET	}
231*da933f89SLORTET
232*da933f89SLORTET	private function notifyIconUpdated(string $targetPage, string $action, string $mediaID = ''): void
233*da933f89SLORTET	{
234*da933f89SLORTET		/** @var helper_plugin_pagesicon|null $helper */
235*da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
236*da933f89SLORTET		if ($helper && method_exists($helper, 'notifyIconUpdated')) {
237*da933f89SLORTET			$helper->notifyIconUpdated($targetPage, $action, $mediaID);
238*da933f89SLORTET			return;
239*da933f89SLORTET		}
240*da933f89SLORTET
241*da933f89SLORTET		global $conf;
242*da933f89SLORTET		@io_saveFile($conf['cachedir'] . '/purgefile', time());
243*da933f89SLORTET		$data = [
244*da933f89SLORTET			'target_page' => cleanID($targetPage),
245*da933f89SLORTET			'action' => $action,
246*da933f89SLORTET			'media_id' => cleanID($mediaID),
247*da933f89SLORTET		];
248*da933f89SLORTET		\dokuwiki\Extension\Event::createAndTrigger('PLUGIN_PAGESICON_UPDATED', $data);
249*da933f89SLORTET	}
250*da933f89SLORTET
251*da933f89SLORTET	private function handleDeletePost(): void
252*da933f89SLORTET	{
253*da933f89SLORTET		global $INPUT, $ID;
254*da933f89SLORTET
255*da933f89SLORTET		if (!$INPUT->post->has('pagesicon_delete_submit')) return;
256*da933f89SLORTET		if (!checkSecurityToken()) return;
257*da933f89SLORTET
258*da933f89SLORTET		$targetPage = cleanID((string)$ID);
259*da933f89SLORTET		$mediaID = cleanID($INPUT->post->str('media_id'));
260*da933f89SLORTET
261*da933f89SLORTET		if ($targetPage === '' || $mediaID === '') {
262*da933f89SLORTET			msg($this->getLang('error_delete_invalid'), -1);
263*da933f89SLORTET			return;
264*da933f89SLORTET		}
265*da933f89SLORTET		if (!$this->canUploadToTarget($targetPage)) {
266*da933f89SLORTET			msg($this->getLang('error_no_upload_permission'), -1);
267*da933f89SLORTET			return;
268*da933f89SLORTET		}
269*da933f89SLORTET		$namespace = getNS($targetPage);
270*da933f89SLORTET		$pageID = noNS($targetPage);
271*da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
272*da933f89SLORTET		$currentBig = ($helper && method_exists($helper, 'getPageImage')) ? (string)$helper->getPageImage($namespace, $pageID, 'big') : '';
273*da933f89SLORTET		$currentSmall = ($helper && method_exists($helper, 'getPageImage')) ? (string)$helper->getPageImage($namespace, $pageID, 'small') : '';
274*da933f89SLORTET		$allowed = array_values(array_filter(array_unique([$currentBig, $currentSmall])));
275*da933f89SLORTET		if (!$allowed || !in_array($mediaID, $allowed, true)) {
276*da933f89SLORTET			msg($this->getLang('error_delete_invalid'), -1);
277*da933f89SLORTET			return;
278*da933f89SLORTET		}
279*da933f89SLORTET
280*da933f89SLORTET		$file = mediaFN($mediaID);
281*da933f89SLORTET		if (!@file_exists($file)) {
282*da933f89SLORTET			msg($this->getLang('error_delete_not_found'), -1);
283*da933f89SLORTET			return;
284*da933f89SLORTET		}
285*da933f89SLORTET		if (!@unlink($file)) {
286*da933f89SLORTET			msg($this->getLang('error_delete_failed'), -1);
287*da933f89SLORTET			return;
288*da933f89SLORTET		}
289*da933f89SLORTET
290*da933f89SLORTET		$this->notifyIconUpdated($targetPage, 'delete', $mediaID);
291*da933f89SLORTET		msg(sprintf($this->getLang('delete_success'), hsc($mediaID)), 1);
292*da933f89SLORTET	}
293*da933f89SLORTET
294*da933f89SLORTET	private function handleUploadPost(): void
295*da933f89SLORTET	{
296*da933f89SLORTET		global $INPUT, $ID;
297*da933f89SLORTET
298*da933f89SLORTET		if (!$INPUT->post->has('pagesicon_upload_submit')) return;
299*da933f89SLORTET		if (!checkSecurityToken()) return;
300*da933f89SLORTET
301*da933f89SLORTET		$targetPage = cleanID((string)$ID);
302*da933f89SLORTET		if (!$this->canUploadToTarget($targetPage)) {
303*da933f89SLORTET			msg($this->getLang('error_no_upload_permission'), -1);
304*da933f89SLORTET			return;
305*da933f89SLORTET		}
306*da933f89SLORTET
307*da933f89SLORTET		$variant = strtolower($INPUT->post->str('icon_variant'));
308*da933f89SLORTET		if (!in_array($variant, ['big', 'small'], true)) {
309*da933f89SLORTET			$variant = 'big';
310*da933f89SLORTET		}
311*da933f89SLORTET
312*da933f89SLORTET		if (!isset($_FILES['pagesicon_file']) || !is_array($_FILES['pagesicon_file'])) {
313*da933f89SLORTET			msg($this->getLang('error_missing_file'), -1);
314*da933f89SLORTET			return;
315*da933f89SLORTET		}
316*da933f89SLORTET
317*da933f89SLORTET		$upload = $_FILES['pagesicon_file'];
318*da933f89SLORTET		if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
319*da933f89SLORTET			msg($this->getLang('error_upload_failed') . ' (' . (int)($upload['error'] ?? -1) . ')', -1);
320*da933f89SLORTET			return;
321*da933f89SLORTET		}
322*da933f89SLORTET
323*da933f89SLORTET		$originalName = (string)($upload['name'] ?? '');
324*da933f89SLORTET		$tmpName = (string)($upload['tmp_name'] ?? '');
325*da933f89SLORTET		if ($tmpName === '' || !is_uploaded_file($tmpName)) {
326*da933f89SLORTET			msg($this->getLang('error_upload_failed'), -1);
327*da933f89SLORTET			return;
328*da933f89SLORTET		}
329*da933f89SLORTET
330*da933f89SLORTET		$ext = strtolower((string)pathinfo($originalName, PATHINFO_EXTENSION));
331*da933f89SLORTET		if ($ext === '') {
332*da933f89SLORTET			msg($this->getLang('error_extension_missing'), -1);
333*da933f89SLORTET			return;
334*da933f89SLORTET		}
335*da933f89SLORTET
336*da933f89SLORTET		$allowed = $this->getAllowedExtensions();
337*da933f89SLORTET		if (!in_array($ext, $allowed, true)) {
338*da933f89SLORTET			msg(sprintf($this->getLang('error_extension_not_allowed'), hsc($ext), hsc(implode(', ', $allowed))), -1);
339*da933f89SLORTET			return;
340*da933f89SLORTET		}
341*da933f89SLORTET
342*da933f89SLORTET		$choices = $this->getUploadNameChoices($targetPage, $variant);
343*da933f89SLORTET		$base = $this->getPostedBaseName($choices);
344*da933f89SLORTET		$namespace = getNS($targetPage);
345*da933f89SLORTET		$mediaBase = $namespace !== '' ? ($namespace . ':' . $base) : $base;
346*da933f89SLORTET		$mediaID = cleanID($mediaBase . '.' . $ext);
347*da933f89SLORTET		$targetFile = mediaFN($mediaID);
348*da933f89SLORTET
349*da933f89SLORTET		io_makeFileDir($targetFile);
350*da933f89SLORTET		if (!@is_dir(dirname($targetFile))) {
351*da933f89SLORTET			msg($this->getLang('error_write_dir'), -1);
352*da933f89SLORTET			return;
353*da933f89SLORTET		}
354*da933f89SLORTET
355*da933f89SLORTET		$moved = @move_uploaded_file($tmpName, $targetFile);
356*da933f89SLORTET		if (!$moved) {
357*da933f89SLORTET			$moved = @copy($tmpName, $targetFile);
358*da933f89SLORTET		}
359*da933f89SLORTET		if (!$moved) {
360*da933f89SLORTET			msg($this->getLang('error_write_file'), -1);
361*da933f89SLORTET			return;
362*da933f89SLORTET		}
363*da933f89SLORTET
364*da933f89SLORTET		@chmod($targetFile, 0664);
365*da933f89SLORTET		$this->notifyIconUpdated($targetPage, 'upload', $mediaID);
366*da933f89SLORTET		msg(sprintf($this->getLang('upload_success'), hsc($mediaID)), 1);
367*da933f89SLORTET	}
368*da933f89SLORTET
369*da933f89SLORTET	private function renderUploadForm(): void
370*da933f89SLORTET	{
371*da933f89SLORTET		global $ID, $INPUT;
372*da933f89SLORTET
373*da933f89SLORTET		$defaultTarget = $this->getDefaultTarget();
374*da933f89SLORTET		$defaultVariant = $this->getDefaultVariant();
375*da933f89SLORTET		$allowed = implode(', ', $this->getAllowedExtensions());
376*da933f89SLORTET		$currentChoices = $this->getUploadNameChoices($defaultTarget, $defaultVariant);
377*da933f89SLORTET		$selectedBase = $this->normalizeBaseName($INPUT->str('icon_filename'));
378*da933f89SLORTET		if (!isset($currentChoices[$selectedBase])) {
379*da933f89SLORTET			$selectedBase = (string)array_key_first($currentChoices);
380*da933f89SLORTET		}
381*da933f89SLORTET		$bigTemplates = json_encode($this->getVariantTemplates('big'));
382*da933f89SLORTET		$smallTemplates = json_encode($this->getVariantTemplates('small'));
383*da933f89SLORTET		$filenameHelp = hsc($this->getLang('icon_filename_help'));
384*da933f89SLORTET		$actionPage = $defaultTarget !== '' ? $defaultTarget : cleanID((string)$ID);
385*da933f89SLORTET		$namespace = getNS($defaultTarget);
386*da933f89SLORTET		$pageID = noNS($defaultTarget);
387*da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
388*da933f89SLORTET		$currentBig = ($helper && method_exists($helper, 'getPageImage')) ? $helper->getPageImage($namespace, $pageID, 'big') : false;
389*da933f89SLORTET		$currentSmall = ($helper && method_exists($helper, 'getPageImage')) ? $helper->getPageImage($namespace, $pageID, 'small') : false;
390*da933f89SLORTET
391*da933f89SLORTET		echo '<h1>' . hsc($this->getLang('menu')) . '</h1>';
392*da933f89SLORTET		echo '<p>' . hsc($this->getLang('intro')) . '</p>';
393*da933f89SLORTET		echo '<p><small>' . hsc(sprintf($this->getLang('allowed_extensions'), $allowed)) . '</small></p>';
394*da933f89SLORTET		echo '<div class="pagesicon-current-preview" style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;margin:10px 0 16px;">';
395*da933f89SLORTET		echo '<div class="pagesicon-current-item">';
396*da933f89SLORTET		echo '<strong>' . hsc($this->getLang('current_big_icon')) . '</strong><br />';
397*da933f89SLORTET		if ($currentBig) {
398*da933f89SLORTET			echo '<a href="' . hsc($this->getMediaManagerUrl($defaultTarget)) . '" target="_blank" title="' . hsc($this->getLang('open_media_manager')) . '">';
399*da933f89SLORTET			echo '<img src="' . ml($currentBig, ['w' => 55]) . '" alt="" width="55" style="display:block;margin:6px 0;" />';
400*da933f89SLORTET			echo '</a>';
401*da933f89SLORTET			echo '<small>' . hsc(noNS($currentBig)) . '</small>';
402*da933f89SLORTET			echo '<form action="' . wl($actionPage) . '" method="post" style="margin-top:6px;">';
403*da933f89SLORTET			formSecurityToken();
404*da933f89SLORTET			echo '<input type="hidden" name="do" value="pagesicon" />';
405*da933f89SLORTET			echo '<input type="hidden" name="media_id" value="' . hsc($currentBig) . '" />';
406*da933f89SLORTET			echo '<input type="hidden" name="pagesicon_delete_submit" value="1" />';
407*da933f89SLORTET			echo '<button type="submit" class="button">' . hsc($this->getLang('delete_icon')) . '</button>';
408*da933f89SLORTET			echo '</form>';
409*da933f89SLORTET		} else {
410*da933f89SLORTET			echo '<small>' . hsc($this->getLang('current_icon_none')) . '</small>';
411*da933f89SLORTET		}
412*da933f89SLORTET		echo '</div>';
413*da933f89SLORTET		echo '<div class="pagesicon-current-item">';
414*da933f89SLORTET		echo '<strong>' . hsc($this->getLang('current_small_icon')) . '</strong><br />';
415*da933f89SLORTET		if ($currentSmall) {
416*da933f89SLORTET			echo '<a href="' . hsc($this->getMediaManagerUrl($defaultTarget)) . '" target="_blank" title="' . hsc($this->getLang('open_media_manager')) . '">';
417*da933f89SLORTET			echo '<img src="' . ml($currentSmall, ['w' => 55]) . '" alt="" width="55" style="display:block;margin:6px 0;" />';
418*da933f89SLORTET			echo '</a>';
419*da933f89SLORTET			echo '<small>' . hsc(noNS($currentSmall)) . '</small>';
420*da933f89SLORTET			echo '<form action="' . wl($actionPage) . '" method="post" style="margin-top:6px;">';
421*da933f89SLORTET			formSecurityToken();
422*da933f89SLORTET			echo '<input type="hidden" name="do" value="pagesicon" />';
423*da933f89SLORTET			echo '<input type="hidden" name="media_id" value="' . hsc($currentSmall) . '" />';
424*da933f89SLORTET			echo '<input type="hidden" name="pagesicon_delete_submit" value="1" />';
425*da933f89SLORTET			echo '<button type="submit" class="button">' . hsc($this->getLang('delete_icon')) . '</button>';
426*da933f89SLORTET			echo '</form>';
427*da933f89SLORTET		} else {
428*da933f89SLORTET			echo '<small>' . hsc($this->getLang('current_icon_none')) . '</small>';
429*da933f89SLORTET		}
430*da933f89SLORTET		echo '</div>';
431*da933f89SLORTET		echo '</div>';
432*da933f89SLORTET
433*da933f89SLORTET		echo '<form action="' . wl($actionPage) . '" method="post" enctype="multipart/form-data">';
434*da933f89SLORTET		formSecurityToken();
435*da933f89SLORTET		echo '<input type="hidden" name="do" value="pagesicon" />';
436*da933f89SLORTET		echo '<input type="hidden" name="pagesicon_upload_submit" value="1" />';
437*da933f89SLORTET
438*da933f89SLORTET		echo '<div class="table"><table class="inline">';
439*da933f89SLORTET		echo '<tr>';
440*da933f89SLORTET		echo '<td class="label"><label for="pagesicon_icon_variant">' . hsc($this->getLang('icon_variant')) . '</label></td>';
441*da933f89SLORTET		echo '<td>';
442*da933f89SLORTET		echo '<select id="pagesicon_icon_variant" name="icon_variant" class="edit">';
443*da933f89SLORTET		echo '<option value="big"' . ($defaultVariant === 'big' ? ' selected="selected"' : '') . '>' . hsc($this->getLang('icon_variant_big')) . '</option>';
444*da933f89SLORTET		echo '<option value="small"' . ($defaultVariant === 'small' ? ' selected="selected"' : '') . '>' . hsc($this->getLang('icon_variant_small')) . '</option>';
445*da933f89SLORTET		echo '</select>';
446*da933f89SLORTET		echo '</td>';
447*da933f89SLORTET		echo '</tr>';
448*da933f89SLORTET
449*da933f89SLORTET		echo '<tr>';
450*da933f89SLORTET		echo '<td class="label"><label for="pagesicon_file">' . hsc($this->getLang('file')) . '</label></td>';
451*da933f89SLORTET		echo '<td><input type="file" id="pagesicon_file" name="pagesicon_file" class="edit" required /></td>';
452*da933f89SLORTET		echo '</tr>';
453*da933f89SLORTET
454*da933f89SLORTET		echo '<tr>';
455*da933f89SLORTET		echo '<td class="label"><label for="pagesicon_icon_filename">' . hsc($this->getLang('icon_filename')) . '</label></td>';
456*da933f89SLORTET		echo '<td>';
457*da933f89SLORTET		echo '<select id="pagesicon_icon_filename" name="icon_filename" class="edit">';
458*da933f89SLORTET		foreach ($currentChoices as $value => $label) {
459*da933f89SLORTET			$selected = $value === $selectedBase ? ' selected="selected"' : '';
460*da933f89SLORTET			echo '<option value="' . hsc($value) . '"' . $selected . '>' . hsc($label) . '</option>';
461*da933f89SLORTET		}
462*da933f89SLORTET		echo '</select>';
463*da933f89SLORTET		echo '<br /><small>' . $filenameHelp . '</small>';
464*da933f89SLORTET		echo '</td>';
465*da933f89SLORTET		echo '</tr>';
466*da933f89SLORTET		echo '</table></div>';
467*da933f89SLORTET
468*da933f89SLORTET		echo '<p><button type="submit" class="button">' . hsc($this->getLang('upload_button')) . '</button></p>';
469*da933f89SLORTET		echo '</form>';
470*da933f89SLORTET
471*da933f89SLORTET		echo '<script>(function(){'
472*da933f89SLORTET			. 'var variant=document.getElementById("pagesicon_icon_variant");'
473*da933f89SLORTET			. 'var filename=document.getElementById("pagesicon_icon_filename");'
474*da933f89SLORTET			. 'if(!variant||!filename)return;'
475*da933f89SLORTET			. 'var pageName=' . json_encode(noNS($defaultTarget)) . ';'
476*da933f89SLORTET			. 'var templates={big:' . $bigTemplates . ',small:' . $smallTemplates . '};'
477*da933f89SLORTET			. 'function cleanBase(name){name=(name||"").trim();if(!name)return"";'
478*da933f89SLORTET			. 'var parts=name.split(":");name=parts[parts.length-1]||"";'
479*da933f89SLORTET			. 'name=name.replace(/\\.[a-z0-9]+$/i,"");'
480*da933f89SLORTET			. 'name=name.replace(/[^a-zA-Z0-9_\\-]/g,"_").replace(/^_+|_+$/g,"");'
481*da933f89SLORTET			. 'return name;}'
482*da933f89SLORTET			. 'function updateChoices(){'
483*da933f89SLORTET			. 'var selected=filename.value;filename.innerHTML="";'
484*da933f89SLORTET			. 'var variantKey=(variant.value==="small")?"small":"big";'
485*da933f89SLORTET			. 'var seen={};'
486*da933f89SLORTET			. '(templates[variantKey]||[]).forEach(function(tpl){'
487*da933f89SLORTET			. 'var resolved=String(tpl||"").replace(/~pagename~/g,pageName);'
488*da933f89SLORTET			. 'var base=cleanBase(resolved);if(!base||seen[base])return;seen[base]=true;'
489*da933f89SLORTET			. 'var opt=document.createElement("option");opt.value=base;opt.textContent=base+".ext";filename.appendChild(opt);'
490*da933f89SLORTET			. '});'
491*da933f89SLORTET			. 'if(!filename.options.length){var fb=variantKey==="small"?"thumbnail":"icon";'
492*da933f89SLORTET			. 'var o=document.createElement("option");o.value=fb;o.textContent=fb+".ext";filename.appendChild(o);}'
493*da933f89SLORTET			. 'for(var i=0;i<filename.options.length;i++){if(filename.options[i].value===selected){filename.selectedIndex=i;return;}}'
494*da933f89SLORTET			. 'filename.selectedIndex=0;'
495*da933f89SLORTET			. '}'
496*da933f89SLORTET			. 'variant.addEventListener("change",updateChoices);'
497*da933f89SLORTET			. 'updateChoices();'
498*da933f89SLORTET			. '})();</script>';
499*da933f89SLORTET	}
500*da933f89SLORTET
501*da933f89SLORTET    public function _displaypageicon(Doku_Event &$event, $param) {
502*da933f89SLORTET        global $ACT, $ID;
503*da933f89SLORTET
504*da933f89SLORTET		if($ACT !== 'show') return;
505*da933f89SLORTET		if(!(bool)$this->getConf('show_on_top')) return;
506*da933f89SLORTET
507*da933f89SLORTET		$pageID = noNS($ID);
508*da933f89SLORTET		if($pageID === 'sidebar' || $pageID === 'footer') return;
509*da933f89SLORTET
510*da933f89SLORTET		$namespace = getNS($ID);
511*da933f89SLORTET		$pageID = noNS((string)$ID);
512*da933f89SLORTET		/** @var helper_plugin_pagesicon|null $helper */
513*da933f89SLORTET		$helper = plugin_load('helper', 'pagesicon');
514*da933f89SLORTET		if(!$helper) return;
515*da933f89SLORTET		$sizeMode = $this->getIconSize() > 35 ? 'bigorsmall' : 'smallorbig';
516*da933f89SLORTET		$logoMediaID = $helper->getPageImage($namespace, $pageID, $sizeMode);
517*da933f89SLORTET		if(!$logoMediaID) return;
518*da933f89SLORTET		if($this->hasIconAlready($event->data, $logoMediaID)) return;
519*da933f89SLORTET
520*da933f89SLORTET		$size = $this->getIconSize();
521*da933f89SLORTET		$src = (string)ml($logoMediaID, ['w' => $size]);
522*da933f89SLORTET		$src = $this->addVersionToUrl($src, $this->getMediaVersionStamp($logoMediaID), true);
523*da933f89SLORTET		if(!$src) return;
524*da933f89SLORTET		$iconHtml = '<img src="' . $src . '" class="media pagesicon-image" loading="lazy" alt="" width="' . $size . '" />';
525*da933f89SLORTET		if ((bool)$this->getConf('show_as_favicon')) {
526*da933f89SLORTET			$favicon = html_entity_decode((string)ml($logoMediaID, ['w' => 32]), ENT_QUOTES | ENT_HTML5, 'UTF-8');
527*da933f89SLORTET			$favicon = $this->addVersionToUrl($favicon, $this->getMediaVersionStamp($logoMediaID), false);
528*da933f89SLORTET			$this->injectFaviconRuntimeScript($event->data, $favicon);
529*da933f89SLORTET		}
530*da933f89SLORTET
531*da933f89SLORTET		$inlineIcon = '<span class="pagesicon-injected pagesicon-injected-inline">' . $iconHtml . '</span> ';
532*da933f89SLORTET		$updated = preg_replace('/<h1\b([^>]*)>/i', '<h1$1>' . $inlineIcon, $event->data, 1, $count);
533*da933f89SLORTET		if ($count > 0 && $updated !== null) {
534*da933f89SLORTET			$event->data = $updated;
535*da933f89SLORTET			return;
536*da933f89SLORTET		}
537*da933f89SLORTET
538*da933f89SLORTET		// Fallback: no H1 found, keep old behavior
539*da933f89SLORTET		$event->data = '<div class="pagesicon-injected">' . $iconHtml . '</div>' . "\n" . $event->data;
540*da933f89SLORTET	}
541*da933f89SLORTET
542*da933f89SLORTET	public function handleAction(Doku_Event $event): void
543*da933f89SLORTET	{
544*da933f89SLORTET		if ($event->data !== 'pagesicon') return;
545*da933f89SLORTET		$event->preventDefault();
546*da933f89SLORTET	}
547*da933f89SLORTET
548*da933f89SLORTET	public function renderAction(Doku_Event $event): void
549*da933f89SLORTET	{
550*da933f89SLORTET		global $ACT;
551*da933f89SLORTET		if ($ACT !== 'pagesicon') return;
552*da933f89SLORTET
553*da933f89SLORTET		$this->handleDeletePost();
554*da933f89SLORTET		$this->handleUploadPost();
555*da933f89SLORTET		$this->renderUploadForm();
556*da933f89SLORTET
557*da933f89SLORTET		$event->preventDefault();
558*da933f89SLORTET		$event->stopPropagation();
559*da933f89SLORTET	}
560*da933f89SLORTET}
561