1*9949c4fdSJason<?php 2*9949c4fdSJason 3*9949c4fdSJasonuse dokuwiki\Extension\ActionPlugin; 4*9949c4fdSJasonuse dokuwiki\Form\Form; 5*9949c4fdSJason 6*9949c4fdSJason/** 7*9949c4fdSJason * Class action_plugin_codeblockedit 8*9949c4fdSJason * 9*9949c4fdSJason * Handles editing of code blocks via standard DokuWiki editor 10*9949c4fdSJason * Uses the same range-based approach as DokuWiki's section editing 11*9949c4fdSJason */ 12*9949c4fdSJasonclass action_plugin_codeblockedit extends ActionPlugin 13*9949c4fdSJason{ 14*9949c4fdSJason /** @var array|null Cached block info to avoid duplicate rawWiki calls */ 15*9949c4fdSJason protected $cachedBlock = null; 16*9949c4fdSJason 17*9949c4fdSJason /** @var int Cached block index */ 18*9949c4fdSJason protected $cachedIndex = -1; 19*9949c4fdSJason 20*9949c4fdSJason /** 21*9949c4fdSJason * Register handlers 22*9949c4fdSJason * 23*9949c4fdSJason * @param Doku_Event_Handler $controller 24*9949c4fdSJason */ 25*9949c4fdSJason public function register(Doku_Event_Handler $controller) 26*9949c4fdSJason { 27*9949c4fdSJason // Set up range before Edit action processes it 28*9949c4fdSJason $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreprocess', null, 1); 29*9949c4fdSJason // Add hidden field to form 30*9949c4fdSJason $controller->register_hook('EDIT_FORM_ADDTEXTAREA', 'BEFORE', $this, 'handleAddTextarea'); 31*9949c4fdSJason // Inject edit permission info into JSINFO 32*9949c4fdSJason $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'addJsInfo'); 33*9949c4fdSJason // Handle preview rendering 34*9949c4fdSJason $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'handlePreview'); 35*9949c4fdSJason } 36*9949c4fdSJason 37*9949c4fdSJason /** 38*9949c4fdSJason * Add edit permission info to JSINFO for JavaScript 39*9949c4fdSJason * 40*9949c4fdSJason * @param Doku_Event $event 41*9949c4fdSJason * @param mixed $param 42*9949c4fdSJason */ 43*9949c4fdSJason public function addJsInfo(Doku_Event $event, $param) 44*9949c4fdSJason { 45*9949c4fdSJason global $JSINFO, $INFO; 46*9949c4fdSJason 47*9949c4fdSJason // Let JavaScript know if user can edit this page 48*9949c4fdSJason $JSINFO['codeblockedit_canedit'] = !empty($INFO['writable']); 49*9949c4fdSJason } 50*9949c4fdSJason 51*9949c4fdSJason /** 52*9949c4fdSJason * Handle preview - wrap $TEXT with code/file tags for proper rendering 53*9949c4fdSJason * 54*9949c4fdSJason * @param Doku_Event $event 55*9949c4fdSJason * @param mixed $param 56*9949c4fdSJason */ 57*9949c4fdSJason public function handlePreview(Doku_Event $event, $param) 58*9949c4fdSJason { 59*9949c4fdSJason global $INPUT, $TEXT, $ID; 60*9949c4fdSJason 61*9949c4fdSJason // Only handle preview action when editing a code block 62*9949c4fdSJason if ($event->data !== 'preview' || !$INPUT->has('codeblockindex')) { 63*9949c4fdSJason return; 64*9949c4fdSJason } 65*9949c4fdSJason 66*9949c4fdSJason // Get the block info to find the tags (use cache if available) 67*9949c4fdSJason $index = $INPUT->int('codeblockindex'); 68*9949c4fdSJason if ($index < 0) { 69*9949c4fdSJason return; 70*9949c4fdSJason } 71*9949c4fdSJason 72*9949c4fdSJason $block = $this->getBlockInfo($ID, $index); 73*9949c4fdSJason if ($block && !empty($block['openTag']) && !empty($block['closeTag'])) { 74*9949c4fdSJason // Wrap the TEXT with the original tags for proper preview rendering 75*9949c4fdSJason $TEXT = $block['openTag'] . $TEXT . $block['closeTag']; 76*9949c4fdSJason } 77*9949c4fdSJason } 78*9949c4fdSJason 79*9949c4fdSJason /** 80*9949c4fdSJason * Handle ACTION_ACT_PREPROCESS 81*9949c4fdSJason * 82*9949c4fdSJason * Sets up $RANGE based on codeblockindex so DokuWiki's native 83*9949c4fdSJason * section editing mechanism handles the rest 84*9949c4fdSJason * 85*9949c4fdSJason * @param Doku_Event $event 86*9949c4fdSJason * @param mixed $param 87*9949c4fdSJason */ 88*9949c4fdSJason public function handlePreprocess(Doku_Event $event, $param) 89*9949c4fdSJason { 90*9949c4fdSJason global $INPUT, $ID, $RANGE; 91*9949c4fdSJason 92*9949c4fdSJason // Only run if codeblockindex is present 93*9949c4fdSJason if (!$INPUT->has('codeblockindex')) { 94*9949c4fdSJason return; 95*9949c4fdSJason } 96*9949c4fdSJason 97*9949c4fdSJason $act = $event->data; 98*9949c4fdSJason if (is_array($act)) { 99*9949c4fdSJason $act = key($act); 100*9949c4fdSJason } 101*9949c4fdSJason 102*9949c4fdSJason // Only process for edit action when no range is set yet 103*9949c4fdSJason if ($act === 'edit' && empty($RANGE)) { 104*9949c4fdSJason // Validate index - must be non-negative integer 105*9949c4fdSJason $index = $INPUT->int('codeblockindex'); 106*9949c4fdSJason if ($index < 0) { 107*9949c4fdSJason msg('Invalid code block index.', -1); 108*9949c4fdSJason return; 109*9949c4fdSJason } 110*9949c4fdSJason 111*9949c4fdSJason $block = $this->getBlockInfo($ID, $index); 112*9949c4fdSJason 113*9949c4fdSJason if ($block) { 114*9949c4fdSJason // Set the RANGE global - DokuWiki will use this to slice the content 115*9949c4fdSJason // Range is 1-based and inclusive on both ends 116*9949c4fdSJason // DokuWiki's rawWikiSlices() subtracts 1 from both start and end 117*9949c4fdSJason // So we need: start+1 for 1-based, end+1 to make end inclusive 118*9949c4fdSJason $RANGE = ($block['start'] + 1) . '-' . ($block['end'] + 1); 119*9949c4fdSJason } else { 120*9949c4fdSJason msg('Code block not found.', -1); 121*9949c4fdSJason } 122*9949c4fdSJason } 123*9949c4fdSJason } 124*9949c4fdSJason 125*9949c4fdSJason /** 126*9949c4fdSJason * Handle EDIT_FORM_ADDTEXTAREA 127*9949c4fdSJason * 128*9949c4fdSJason * Adds the hidden codeblockindex and hid fields to the form 129*9949c4fdSJason * 130*9949c4fdSJason * @param Doku_Event $event 131*9949c4fdSJason * @param mixed $param 132*9949c4fdSJason */ 133*9949c4fdSJason public function handleAddTextarea(Doku_Event $event, $param) 134*9949c4fdSJason { 135*9949c4fdSJason global $INPUT; 136*9949c4fdSJason 137*9949c4fdSJason if ($INPUT->has('codeblockindex')) { 138*9949c4fdSJason /** @var Form $form */ 139*9949c4fdSJason $form = $event->data['form']; 140*9949c4fdSJason $form->setHiddenField('codeblockindex', $INPUT->int('codeblockindex')); 141*9949c4fdSJason 142*9949c4fdSJason // Pass hid through for redirect back to code block after save 143*9949c4fdSJason // Sanitize hid to only allow valid anchor format (codeblock_N) 144*9949c4fdSJason if ($INPUT->has('hid')) { 145*9949c4fdSJason $hid = $INPUT->str('hid'); 146*9949c4fdSJason if (preg_match('/^codeblock_\d+$/', $hid)) { 147*9949c4fdSJason $form->setHiddenField('hid', $hid); 148*9949c4fdSJason } 149*9949c4fdSJason } 150*9949c4fdSJason } 151*9949c4fdSJason } 152*9949c4fdSJason 153*9949c4fdSJason /** 154*9949c4fdSJason * Get block info with caching to avoid duplicate rawWiki calls 155*9949c4fdSJason * 156*9949c4fdSJason * @param string $id Page ID 157*9949c4fdSJason * @param int $index Block index (0-based) 158*9949c4fdSJason * @return array|null Block info or null if not found 159*9949c4fdSJason */ 160*9949c4fdSJason protected function getBlockInfo($id, $index) 161*9949c4fdSJason { 162*9949c4fdSJason // Return cached result if available 163*9949c4fdSJason if ($this->cachedIndex === $index && $this->cachedBlock !== null) { 164*9949c4fdSJason return $this->cachedBlock; 165*9949c4fdSJason } 166*9949c4fdSJason 167*9949c4fdSJason $text = rawWiki($id); 168*9949c4fdSJason if (empty($text)) { 169*9949c4fdSJason return null; 170*9949c4fdSJason } 171*9949c4fdSJason 172*9949c4fdSJason $this->cachedIndex = $index; 173*9949c4fdSJason $this->cachedBlock = $this->findBlockRange($text, $index); 174*9949c4fdSJason return $this->cachedBlock; 175*9949c4fdSJason } 176*9949c4fdSJason 177*9949c4fdSJason /** 178*9949c4fdSJason * Find the N-th code/file block and return its byte range 179*9949c4fdSJason * 180*9949c4fdSJason * @param string $text Raw wiki text 181*9949c4fdSJason * @param int $index Target index (0-based) 182*9949c4fdSJason * @return array|null ['start' => int, 'end' => int, 'content' => string, 'openTag' => string, 'closeTag' => string] 183*9949c4fdSJason */ 184*9949c4fdSJason protected function findBlockRange($text, $index) 185*9949c4fdSJason { 186*9949c4fdSJason // Normalize line endings for consistent matching 187*9949c4fdSJason $text = str_replace("\r\n", "\n", $text); 188*9949c4fdSJason $text = str_replace("\r", "\n", $text); 189*9949c4fdSJason 190*9949c4fdSJason // Find all code/file blocks using regex 191*9949c4fdSJason // Pattern matches <code ...> or <file ...> blocks 192*9949c4fdSJason $pattern = '/(<(?:code|file)[^>]*>)(.*?)(<\/(?:code|file)>)/s'; 193*9949c4fdSJason 194*9949c4fdSJason if (preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) { 195*9949c4fdSJason if (isset($matches[2][$index])) { 196*9949c4fdSJason // $matches[2][$index][0] = content 197*9949c4fdSJason // $matches[2][$index][1] = offset of content start 198*9949c4fdSJason $content = $matches[2][$index][0]; 199*9949c4fdSJason $contentStart = $matches[2][$index][1]; 200*9949c4fdSJason $contentEnd = $contentStart + strlen($content); 201*9949c4fdSJason 202*9949c4fdSJason // Also capture opening and closing tags for preview rendering 203*9949c4fdSJason $openTag = $matches[1][$index][0]; 204*9949c4fdSJason $closeTag = $matches[3][$index][0]; 205*9949c4fdSJason 206*9949c4fdSJason return [ 207*9949c4fdSJason 'start' => $contentStart, 208*9949c4fdSJason 'end' => $contentEnd, 209*9949c4fdSJason 'content' => $content, 210*9949c4fdSJason 'openTag' => $openTag, 211*9949c4fdSJason 'closeTag' => $closeTag 212*9949c4fdSJason ]; 213*9949c4fdSJason } 214*9949c4fdSJason } 215*9949c4fdSJason 216*9949c4fdSJason return null; 217*9949c4fdSJason } 218*9949c4fdSJason} 219*9949c4fdSJason 220