1<?php 2/** 3 * Templater Plugin: Based from the include plugin, like MediaWiki's template 4 * Usage: 5 * {{template>page}} for "page" in same namespace 6 * {{template>:page}} for "page" in top namespace 7 * {{template>namespace:page}} for "page" in namespace "namespace" 8 * {{template>.namespace:page}} for "page" in subnamespace "namespace" 9 * {{template>page#section}} for a section of "page" 10 * 11 * Replacers are handled in a simple key/value pair method: 12 * {{template>page|key=val|key2=val|key3=val}} 13 * 14 * Templates are wiki pages, with replacers being delimited like: 15 * @key1@ @key2@ @key3@ 16 * 17 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 18 * @author Jonathan Arkell <jonnay@jonnay.net> 19 * based on code by Esther Brunner <esther@kaffeehaus.ch> 20 * @maintainer Daniel Dias Rodrigues (aka Nerun) <danieldiasr@gmail.com> 21 * @contributors Vincent de Lau <vincent@delau.nl> 22 * Ximin Luo <xl269@cam.ac.uk> 23 * jack126guy <halfgray7e@gmail.com> 24 * Turq Whiteside <turq@mage.city> 25 */ 26 27use dokuwiki\File\PageResolver; 28 29define('BEGIN_REPLACE_DELIMITER', '@'); 30define('END_REPLACE_DELIMITER', '@'); 31 32require_once('debug.php'); 33 34/** 35 * All DokuWiki plugins to extend the parser/rendering mechanism 36 * need to inherit from this class 37 */ 38class syntax_plugin_templater extends DokuWiki_Syntax_Plugin { 39 /** 40 * What kind of syntax are we? 41 */ 42 function getType() { 43 return 'container'; 44 } 45 46 function getAllowedTypes() { 47 return array('container', 'substition', 'protected', 'disabled', 'formatting'); 48 } 49 50 /** 51 * Where to sort in? 52 */ 53 function getSort() { 54 return 302; 55 } 56 57 /** 58 * Paragraph Type 59 */ 60 function getPType() { 61 return 'block'; 62 } 63 64 /** 65 * Connect pattern to lexer 66 */ 67 function connectTo($mode) { 68 $this->Lexer->addSpecialPattern("{{template>.+?}}", $mode, 'plugin_templater'); 69 } 70 71 /** 72 * Handle the match 73 */ 74 function handle($match, $state, $pos, Doku_Handler $handler) { 75 global $ID; 76 77 $match = substr($match, 11, -2); // strip markup 78 $replacers = preg_split('/(?<!\\\\)\|/', $match); // Get the replacers 79 $wikipage = array_shift($replacers); 80 81 $replacers = $this->_massageReplacers($replacers); 82 83 $wikipage = preg_split('/\#/u', $wikipage, 2); // split hash from filename 84 $parentpage = empty(self::$pagestack)? $ID : end(self::$pagestack); // get correct namespace 85 // resolve shortcuts: 86 $resolver = new PageResolver(getNS($parentpage)); 87 $wikipage[0] = $resolver->resolveId($wikipage[0]); 88 $exists = page_exists($wikipage[0]); 89 90 // check for perrmission 91 if (auth_quickaclcheck($wikipage[0]) < 1) 92 return false; 93 94 // $wikipage[1] is the header of a template enclosed within a section {{template>page#section}} 95 // Not all template calls will be {{template>page#section}}, some will be {{template>page}} 96 // It fix "Undefined array key 1" warning 97 if (array_key_exists(1, $wikipage)) { 98 $section = cleanID($wikipage[1]); 99 } else { 100 $section = null; 101 } 102 103 return array($wikipage[0], $replacers, $section); 104 } 105 106 private static $pagestack = array(); // keep track of recursing template renderings 107 108 /** 109 * Create output 110 * This is a refactoring candidate. Needs to be a little clearer. 111 */ 112 function render($mode, Doku_Renderer $renderer, $data) { 113 if ($mode != 'xhtml') 114 return false; 115 116 if ($data[0] === false) { 117 // False means no permissions 118 $renderer->doc .= '<div class="templater"> '; 119 $renderer->doc .= $this->getLang('no_permissions_view'); 120 $renderer->doc .= ' </div>'; 121 $renderer->info['cache'] = FALSE; 122 return true; 123 } 124 125 $file = wikiFN($data[0]); 126 if (!@file_exists($file)) { 127 $renderer->doc .= '<div class="templater">— '; 128 $renderer->doc .= $this->getLang('template'); 129 $renderer->doc .= ' '; 130 $renderer->internalLink($data[0]); 131 $renderer->doc .= ' '; 132 $renderer->doc .= $this->getLang('not_found'); 133 $renderer->doc .= '<br/><br/></div>'; 134 $renderer->info['cache'] = FALSE; 135 return true; 136 } else if (array_search($data[0], self::$pagestack) !== false) { 137 $renderer->doc .= '<div class="templater">— '; 138 $renderer->doc .= $this->getLang('processing_template'); 139 $renderer->doc .= ' '; 140 $renderer->internalLink($data[0]); 141 $renderer->doc .= ' '; 142 $renderer->doc .= $this->getLang('stopped_recursion'); 143 $renderer->doc .= '<br/><br/></div>'; 144 return true; 145 } 146 self::$pagestack[] = $data[0]; // push this onto the stack 147 148 // Get the raw file, and parse it into its instructions. This could be cached... maybe. 149 $rawFile = io_readfile($file); 150 151 // fill in all known values 152 if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) { 153 $rawFile = str_replace($data[1]['keys'], $data[1]['vals'], $rawFile); 154 } 155 156 // replace unmatched substitutions with "" or use DEFAULT_STR from data arguments if exists. 157 $left_overs = '/'.BEGIN_REPLACE_DELIMITER.'.*'.END_REPLACE_DELIMITER.'/'; 158 159 if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) { 160 $def_key = array_search(BEGIN_REPLACE_DELIMITER."DEFAULT_STR".END_REPLACE_DELIMITER, $data[1]['keys']); 161 $DEFAULT_STR = $def_key ? $data[1]['vals'][$def_key] : ""; 162 $rawFile = preg_replace($left_overs, $DEFAULT_STR, $rawFile); 163 } 164 165 $instr = p_get_instructions($rawFile); 166 167 // filter section if given 168 if ($data[2]) { 169 $getSection = $this->_getSection($data[2], $instr); 170 171 $instr = $getSection[0]; 172 173 if(!is_null($getSection[1])) { 174 $renderer->doc .= sprintf($getSection[1], $data[2]); 175 $renderer->internalLink($data[0]); 176 $renderer->doc .= '.<br/><br/></div>'; 177 } 178 } 179 180 // correct relative internal links and media 181 $instr = $this->_correctRelNS($instr, $data[0]); 182 183 // doesn't show the heading for each template if {{template>page#section}} 184 if (sizeof($instr) > 0 && !isset($getSection[1])) { 185 if (array_key_exists(0, $instr[0][1]) && $instr[0][1][0] == $data[2]) { 186 $instr[0][1][0] = null; 187 } 188 } 189 190 // render the instructructions on the fly 191 $text = p_render('xhtml', $instr, $info); 192 193 // remove toc, section edit buttons and category tags 194 $patterns = array('!<div class="toc">.*?(</div>\n</div>)!s', 195 '#<!-- SECTION \[(\d*-\d*)\] -->#', 196 '!<div class="category">.*?</div>!s'); 197 $replace = array('', '', ''); 198 $text = preg_replace($patterns, $replace, $text); 199 200 // prevent caching to ensure the included page is always fresh 201 $renderer->info['cache'] = FALSE; 202 203 // embed the included page 204 $renderer->doc .= '<div class="templater">'; 205 $renderer->doc .= $text; 206 $renderer->doc .= '</div>'; 207 208 array_pop(self::$pagestack); // pop off the stack when done 209 return true; 210 } 211 212 /** 213 * Get a section including its subsections 214 */ 215 function _getSection($title, $instructions) { 216 $i = (array) null; 217 $level = null; 218 $no_section = null; 219 220 foreach ($instructions as $instruction) { 221 if ($instruction[0] == 'header') { 222 223 // found the right header 224 if (cleanID($instruction[1][0]) == $title) { 225 $level = $instruction[1][1]; 226 $i[] = $instruction; 227 } else { 228 if (isset($level) && isset($i)) { 229 if ($instruction[1][1] > $level) { 230 $i[] = $instruction; 231 // next header of the same level or higher -> exit 232 } else { 233 return array($i,null); 234 } 235 } 236 } 237 } else { // content between headers 238 if (isset($level) && isset($i)) { 239 $i[] = $instruction; 240 } 241 } 242 } 243 244 // Fix for when page#section doesn't exist 245 if(sizeof($i) == 0) { 246 $no_section_begin = '<div class="templater">— '; 247 $no_section_end = $this->getLang('no_such_section'); 248 $no_section = $no_section_begin . $no_section_end . ' '; 249 } 250 251 return array($i,$no_section); 252 } 253 254 /** 255 * Corrects relative internal links and media 256 */ 257 function _correctRelNS($instr, $incl) { 258 global $ID; 259 260 // check if included page is in same namespace 261 $iNS = getNS($incl); 262 if (getNS($ID) == $iNS) 263 return $instr; 264 265 // convert internal links and media from relative to absolute 266 $n = count($instr); 267 for($i = 0; $i < $n; $i++) { 268 if (substr($instr[$i][0], 0, 8) != 'internal') 269 continue; 270 271 // relative subnamespace 272 if ($instr[$i][1][0][0] == '.') { 273 $instr[$i][1][0] = $iNS.':'.substr($instr[$i][1][0], 1); 274 275 // relative link 276 } else if (strpos($instr[$i][1][0], ':') === false) { 277 $instr[$i][1][0] = $iNS.':'.$instr[$i][1][0]; 278 } 279 } 280 281 return $instr; 282 } 283 284 /** 285 * Handles the replacement array 286 */ 287 function _massageReplacers($replacers) { 288 $r = array(); 289 if (is_null($replacers)) { 290 $r['keys'] = null; 291 $r['vals'] = null; 292 } else if (is_string($replacers)) { 293 if ( str_contains($replacers, '=') && (substr(trim($replacers), -1) != '=') ){ 294 list($k, $v) = explode('=', $replacers, 2); 295 $r['keys'] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER; 296 $r['vals'] = trim(str_replace('\|', '|', $v)); 297 } 298 } else if ( is_array($replacers) ) { 299 foreach($replacers as $rep) { 300 if ( str_contains($rep, '=') && (substr(trim($rep), -1) != '=') ){ 301 list($k, $v) = explode('=', $rep, 2); 302 $r['keys'][] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER; 303 if (trim($v)[0] == '"' and trim($v)[-1] == '"') { 304 $r['vals'][] = substr(trim(str_replace('\|','|',$v)), 1, -1); 305 } else { 306 $r['vals'][] = trim(str_replace('\|','|',$v)); 307 } 308 } 309 } 310 } else { 311 // This is an assertion failure. We should NEVER get here. 312 //die("FATAL ERROR! Unknown type passed to syntax_plugin_templater::massageReplaceMentArray() can't message syntax_plugin_templater::\$replacers! Type is:".gettype($r)." Value is:".$r); 313 $r['keys'] = null; 314 $r['vals'] = null; 315 } 316 return $r; 317 } 318} 319