1*3d0502afSRüdiger Kessel<?php 2*3d0502afSRüdiger Kessel/** 3*3d0502afSRüdiger Kessel * DokuWiki Plugin multimage (Syntax Component) 4*3d0502afSRüdiger Kessel */ 5*3d0502afSRüdiger Kessel 6*3d0502afSRüdiger Kesselif (!defined('DOKU_INC')) die(); 7*3d0502afSRüdiger Kessel 8*3d0502afSRüdiger Kesselclass syntax_plugin_multimage extends DokuWiki_Syntax_Plugin 9*3d0502afSRüdiger Kessel{ 10*3d0502afSRüdiger Kessel public function getType(): string 11*3d0502afSRüdiger Kessel { 12*3d0502afSRüdiger Kessel // We capture a delimited block of raw text 13*3d0502afSRüdiger Kessel return 'container'; 14*3d0502afSRüdiger Kessel } 15*3d0502afSRüdiger Kessel 16*3d0502afSRüdiger Kessel public function getPType(): string 17*3d0502afSRüdiger Kessel { 18*3d0502afSRüdiger Kessel // We emit block-level HTML (<div>) 19*3d0502afSRüdiger Kessel return 'block'; 20*3d0502afSRüdiger Kessel } 21*3d0502afSRüdiger Kessel 22*3d0502afSRüdiger Kessel public function getSort(): int 23*3d0502afSRüdiger Kessel { 24*3d0502afSRüdiger Kessel return 200; 25*3d0502afSRüdiger Kessel } 26*3d0502afSRüdiger Kessel 27*3d0502afSRüdiger Kessel public function connectTo($mode): void 28*3d0502afSRüdiger Kessel { 29*3d0502afSRüdiger Kessel $this->Lexer->addEntryPattern( 30*3d0502afSRüdiger Kessel '<images>(?=.*?</images>)', 31*3d0502afSRüdiger Kessel $mode, 32*3d0502afSRüdiger Kessel 'plugin_multimage' 33*3d0502afSRüdiger Kessel ); 34*3d0502afSRüdiger Kessel } 35*3d0502afSRüdiger Kessel 36*3d0502afSRüdiger Kessel public function postConnect(): void 37*3d0502afSRüdiger Kessel { 38*3d0502afSRüdiger Kessel $this->Lexer->addExitPattern('</images>', 'plugin_multimage'); 39*3d0502afSRüdiger Kessel } 40*3d0502afSRüdiger Kessel 41*3d0502afSRüdiger Kessel public function handle($match, $state, $pos, Doku_Handler $handler) 42*3d0502afSRüdiger Kessel { 43*3d0502afSRüdiger Kessel switch ($state) { 44*3d0502afSRüdiger Kessel case DOKU_LEXER_ENTER: 45*3d0502afSRüdiger Kessel return ['enter']; 46*3d0502afSRüdiger Kessel case DOKU_LEXER_UNMATCHED: 47*3d0502afSRüdiger Kessel return ['content', $match]; 48*3d0502afSRüdiger Kessel case DOKU_LEXER_EXIT: 49*3d0502afSRüdiger Kessel return ['exit']; 50*3d0502afSRüdiger Kessel } 51*3d0502afSRüdiger Kessel return []; 52*3d0502afSRüdiger Kessel } 53*3d0502afSRüdiger Kessel 54*3d0502afSRüdiger Kessel public function render($mode, Doku_Renderer $renderer, $data): bool 55*3d0502afSRüdiger Kessel { 56*3d0502afSRüdiger Kessel if ($mode !== 'xhtml') { 57*3d0502afSRüdiger Kessel return false; 58*3d0502afSRüdiger Kessel } 59*3d0502afSRüdiger Kessel 60*3d0502afSRüdiger Kessel [$type, $content] = array_pad($data, 2, null); 61*3d0502afSRüdiger Kessel 62*3d0502afSRüdiger Kessel if ($type !== 'content') { 63*3d0502afSRüdiger Kessel return true; 64*3d0502afSRüdiger Kessel } 65*3d0502afSRüdiger Kessel 66*3d0502afSRüdiger Kessel $yaml = trim($content); 67*3d0502afSRüdiger Kessel if ($yaml === '') { 68*3d0502afSRüdiger Kessel return true; 69*3d0502afSRüdiger Kessel } 70*3d0502afSRüdiger Kessel 71*3d0502afSRüdiger Kessel if (!function_exists('yaml_parse')) { 72*3d0502afSRüdiger Kessel $renderer->doc .= '<!-- multimage: PHP YAML extension missing -->'; 73*3d0502afSRüdiger Kessel return true; 74*3d0502afSRüdiger Kessel } 75*3d0502afSRüdiger Kessel 76*3d0502afSRüdiger Kessel $parsed = @yaml_parse($yaml); 77*3d0502afSRüdiger Kessel if (!is_array($parsed)) { 78*3d0502afSRüdiger Kessel $renderer->doc .= '<!-- multimage: invalid YAML -->'; 79*3d0502afSRüdiger Kessel return true; 80*3d0502afSRüdiger Kessel } 81*3d0502afSRüdiger Kessel 82*3d0502afSRüdiger Kessel /* 83*3d0502afSRüdiger Kessel * Normalization: 84*3d0502afSRüdiger Kessel * - mapping with 'image' => single base image 85*3d0502afSRüdiger Kessel * - sequence => multiple base images 86*3d0502afSRüdiger Kessel */ 87*3d0502afSRüdiger Kessel if (array_key_exists('image', $parsed)) { 88*3d0502afSRüdiger Kessel $baseImages = [$parsed]; 89*3d0502afSRüdiger Kessel } elseif (array_values($parsed) === $parsed) { 90*3d0502afSRüdiger Kessel $baseImages = $parsed; 91*3d0502afSRüdiger Kessel } else { 92*3d0502afSRüdiger Kessel $renderer->doc .= '<!-- multimage: expected image or list of images -->'; 93*3d0502afSRüdiger Kessel return true; 94*3d0502afSRüdiger Kessel } 95*3d0502afSRüdiger Kessel 96*3d0502afSRüdiger Kessel $renderer->doc .= '<div class="images">'; 97*3d0502afSRüdiger Kessel 98*3d0502afSRüdiger Kessel foreach ($baseImages as $baseImage) { 99*3d0502afSRüdiger Kessel if (!is_array($baseImage)) { 100*3d0502afSRüdiger Kessel continue; 101*3d0502afSRüdiger Kessel } 102*3d0502afSRüdiger Kessel 103*3d0502afSRüdiger Kessel $this->renderBaseImage($renderer, $baseImage); 104*3d0502afSRüdiger Kessel 105*3d0502afSRüdiger Kessel 106*3d0502afSRüdiger Kessel if (!empty($baseImage['ontop']) && is_array($baseImage['ontop'])) { 107*3d0502afSRüdiger Kessel foreach ($baseImage['ontop'] as $ontopImage) { 108*3d0502afSRüdiger Kessel if (is_array($ontopImage)) { 109*3d0502afSRüdiger Kessel $this->renderOntopImage($renderer, $ontopImage); 110*3d0502afSRüdiger Kessel } 111*3d0502afSRüdiger Kessel } 112*3d0502afSRüdiger Kessel } 113*3d0502afSRüdiger Kessel } 114*3d0502afSRüdiger Kessel 115*3d0502afSRüdiger Kessel $renderer->doc .= '</div>'; 116*3d0502afSRüdiger Kessel 117*3d0502afSRüdiger Kessel return true; 118*3d0502afSRüdiger Kessel } 119*3d0502afSRüdiger Kessel 120*3d0502afSRüdiger Kessel /* ---------- rendering helpers ---------- */ 121*3d0502afSRüdiger Kessel 122*3d0502afSRüdiger Kessel protected function renderBaseImage(Doku_Renderer $renderer, array $entry): void 123*3d0502afSRüdiger Kessel { 124*3d0502afSRüdiger Kessel $style = $this->filterStyles($entry['style'] ?? null); 125*3d0502afSRüdiger Kessel 126*3d0502afSRüdiger Kessel $renderer->doc .= '<div class="images_ontop">'; 127*3d0502afSRüdiger Kessel 128*3d0502afSRüdiger Kessel if (empty($entry['image'])) { 129*3d0502afSRüdiger Kessel $renderer->doc .= '<div class="images_image"'; 130*3d0502afSRüdiger Kessel if ($style !== '') { 131*3d0502afSRüdiger Kessel $renderer->doc .= ' style="' . hsc($style) . '"'; 132*3d0502afSRüdiger Kessel } 133*3d0502afSRüdiger Kessel $renderer->doc .= '> </div>'; 134*3d0502afSRüdiger Kessel } else { 135*3d0502afSRüdiger Kessel [$src, $alt] = $this->parseImage($entry['image']); 136*3d0502afSRüdiger Kessel $renderer->doc .= '<img class="images_image" src="' . hsc($src) . '"'; 137*3d0502afSRüdiger Kessel if ($style !== '') { 138*3d0502afSRüdiger Kessel $renderer->doc .= ' style="' . hsc($style) . '"'; 139*3d0502afSRüdiger Kessel } 140*3d0502afSRüdiger Kessel $renderer->doc .= ' alt="' . hsc($alt) . '"/>'; 141*3d0502afSRüdiger Kessel } 142*3d0502afSRüdiger Kessel $renderer->doc .= '</div>'; 143*3d0502afSRüdiger Kessel } 144*3d0502afSRüdiger Kessel 145*3d0502afSRüdiger Kessel protected function renderOntopImage(Doku_Renderer $renderer, array $entry): void 146*3d0502afSRüdiger Kessel { 147*3d0502afSRüdiger Kessel if (empty($entry['image'])) { 148*3d0502afSRüdiger Kessel return; 149*3d0502afSRüdiger Kessel } 150*3d0502afSRüdiger Kessel 151*3d0502afSRüdiger Kessel [$src, $alt] = $this->parseImage($entry['image']); 152*3d0502afSRüdiger Kessel $style = $this->filterStyles($entry['style'] ?? null); 153*3d0502afSRüdiger Kessel 154*3d0502afSRüdiger Kessel $renderer->doc .= '<div class="images_ontop">'; 155*3d0502afSRüdiger Kessel $renderer->doc .= '<img class="images_ontop_image" src="' . hsc($src) . '"'; 156*3d0502afSRüdiger Kessel if ($style !== '') { 157*3d0502afSRüdiger Kessel $renderer->doc .= ' style="' . hsc($style) . '"'; 158*3d0502afSRüdiger Kessel } 159*3d0502afSRüdiger Kessel $renderer->doc .= ' alt="' . hsc($alt) . '"/>'; 160*3d0502afSRüdiger Kessel $renderer->doc .= '</div>'; 161*3d0502afSRüdiger Kessel } 162*3d0502afSRüdiger Kessel 163*3d0502afSRüdiger Kessel /* ---------- parsing & validation ---------- */ 164*3d0502afSRüdiger Kessel 165*3d0502afSRüdiger Kessel protected function parseImage(string $spec): array 166*3d0502afSRüdiger Kessel { 167*3d0502afSRüdiger Kessel // Format: wiki:img.svg?400|alt text 168*3d0502afSRüdiger Kessel [$left, $alt] = array_pad(explode('|', $spec, 2), 2, ''); 169*3d0502afSRüdiger Kessel [$id, $params] = array_pad(explode('?', $left, 2), 2, ''); 170*3d0502afSRüdiger Kessel 171*3d0502afSRüdiger Kessel $opts = []; 172*3d0502afSRüdiger Kessel if ($params !== '') { 173*3d0502afSRüdiger Kessel parse_str($params, $opts); 174*3d0502afSRüdiger Kessel } 175*3d0502afSRüdiger Kessel 176*3d0502afSRüdiger Kessel $src = ml($id, $opts, true, '&'); 177*3d0502afSRüdiger Kessel 178*3d0502afSRüdiger Kessel return [$src, $alt]; 179*3d0502afSRüdiger Kessel } 180*3d0502afSRüdiger Kessel 181*3d0502afSRüdiger Kesselprotected function filterStyles($styles): string 182*3d0502afSRüdiger Kessel{ 183*3d0502afSRüdiger Kessel if (!is_array($styles)) { 184*3d0502afSRüdiger Kessel return ''; 185*3d0502afSRüdiger Kessel } 186*3d0502afSRüdiger Kessel 187*3d0502afSRüdiger Kessel $allowed = $this->getAllowedStyles(); 188*3d0502afSRüdiger Kessel $out = []; 189*3d0502afSRüdiger Kessel 190*3d0502afSRüdiger Kessel foreach ($styles as $key => $value) { 191*3d0502afSRüdiger Kessel if (!is_string($key) || !is_scalar($value)) { 192*3d0502afSRüdiger Kessel continue; 193*3d0502afSRüdiger Kessel } 194*3d0502afSRüdiger Kessel 195*3d0502afSRüdiger Kessel $key = trim($key); 196*3d0502afSRüdiger Kessel $value = trim((string)$value); 197*3d0502afSRüdiger Kessel 198*3d0502afSRüdiger Kessel if (!in_array($key, $allowed, true)) { 199*3d0502afSRüdiger Kessel continue; 200*3d0502afSRüdiger Kessel } 201*3d0502afSRüdiger Kessel 202*3d0502afSRüdiger Kessel // Rewrite url("...") or url('...') 203*3d0502afSRüdiger Kessel $value = preg_replace_callback( 204*3d0502afSRüdiger Kessel '/url\(\s*([\'"])([^\'"]+)\1\s*\)/i', 205*3d0502afSRüdiger Kessel function ($m) { 206*3d0502afSRüdiger Kessel $target = trim($m[2]); 207*3d0502afSRüdiger Kessel 208*3d0502afSRüdiger Kessel // Only rewrite DokuWiki media IDs 209*3d0502afSRüdiger Kessel if (preg_match('/^[a-zA-Z0-9_:-]+(\.[a-zA-Z0-9]+)?$/', $target)) { 210*3d0502afSRüdiger Kessel $url = ml($target, [], true, '&'); 211*3d0502afSRüdiger Kessel return 'url("' . $url . '")'; 212*3d0502afSRüdiger Kessel } 213*3d0502afSRüdiger Kessel 214*3d0502afSRüdiger Kessel // Leave untouched if not a media ID 215*3d0502afSRüdiger Kessel return $m[0]; 216*3d0502afSRüdiger Kessel }, 217*3d0502afSRüdiger Kessel $value 218*3d0502afSRüdiger Kessel ); 219*3d0502afSRüdiger Kessel 220*3d0502afSRüdiger Kessel $out[] = $key . ': ' . $value; 221*3d0502afSRüdiger Kessel } 222*3d0502afSRüdiger Kessel 223*3d0502afSRüdiger Kessel return $out ? implode('; ', $out) . ';': ''; 224*3d0502afSRüdiger Kessel} 225*3d0502afSRüdiger Kessel 226*3d0502afSRüdiger Kessel protected function getAllowedStyles(): array 227*3d0502afSRüdiger Kessel { 228*3d0502afSRüdiger Kessel static $styles = null; 229*3d0502afSRüdiger Kessel 230*3d0502afSRüdiger Kessel if ($styles !== null) { 231*3d0502afSRüdiger Kessel return $styles; 232*3d0502afSRüdiger Kessel } 233*3d0502afSRüdiger Kessel 234*3d0502afSRüdiger Kessel $raw = (string)$this->getConf('allowed_styles'); 235*3d0502afSRüdiger Kessel 236*3d0502afSRüdiger Kessel $styles = array_filter( 237*3d0502afSRüdiger Kessel array_map('trim', explode(',', $raw)), 238*3d0502afSRüdiger Kessel static fn($s) => $s !== '' 239*3d0502afSRüdiger Kessel ); 240*3d0502afSRüdiger Kessel 241*3d0502afSRüdiger Kessel return $styles; 242*3d0502afSRüdiger Kessel } 243*3d0502afSRüdiger Kessel} 244*3d0502afSRüdiger Kessel 245