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