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