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 ? '&' : '&'); 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