1<?php 2 3use dokuwiki\File\PageResolver; 4 5/** 6 * Simple template replacement action for the bureaucracy plugin 7 * 8 * @author Michael Klier <chi@chimeric.de> 9 */ 10 11class helper_plugin_bureaucracy_actiontemplate extends helper_plugin_bureaucracy_action { 12 13 var $targetpages; 14 var $pagename; 15 16 /** 17 * Performs template action 18 * 19 * @param helper_plugin_bureaucracy_field[] $fields array with form fields 20 * @param string $thanks thanks message 21 * @param array $argv array with entries: template, pagename, separator 22 * @return array|mixed 23 * 24 * @throws Exception 25 */ 26 public function run($fields, $thanks, $argv) { 27 global $conf; 28 29 [$tpl, $this->pagename] = $argv; 30 $sep = $argv[2] ?? $conf['sepchar']; 31 32 $this->patterns = array(); 33 $this->values = array(); 34 $this->targetpages = array(); 35 36 $this->prepareNamespacetemplateReplacements(); 37 $this->prepareDateTimereplacements(); 38 $this->prepareLanguagePlaceholder(); 39 $this->prepareNoincludeReplacement(); 40 $this->prepareFieldReplacements($fields); 41 42 $evdata = array( 43 'patterns' => &$this->patterns, 44 'values' => &$this->values, 45 'fields' => $fields, 46 'action' => $this 47 ); 48 49 $event = new Doku_Event('PLUGIN_BUREAUCRACY_PAGENAME', $evdata); 50 if ($event->advise_before()) { 51 $this->buildTargetPagename($fields, $sep); 52 } 53 $event->advise_after(); 54 55 //target&template(s) from addpage fields 56 $this->getAdditionalTargetpages($fields); 57 //target&template(s) from action field 58 $tpl = $this->getActionTargetpages($tpl); 59 60 if(empty($this->targetpages)) { 61 throw new Exception(sprintf($this->getLang('e_template'), $tpl)); 62 } 63 64 $this->checkTargetPageNames(); 65 66 $this->processUploads($fields); 67 $this->replaceAndSavePages($fields); 68 69 $ret = $this->buildThankYouPage($thanks); 70 71 return $ret; 72 } 73 74 /** 75 * Prepare and resolve target page 76 * 77 * @param helper_plugin_bureaucracy_field[] $fields List of field objects 78 * @param string $sep Separator between fields for page id 79 * @throws Exception missing pagename 80 */ 81 protected function buildTargetPagename($fields, $sep) { 82 global $ID; 83 84 foreach ($fields as $field) { 85 $pname = $field->getParam('pagename'); 86 if (!is_null($pname)) { 87 if (is_array($pname)) $pname = implode($sep, $pname); 88 $this->pagename .= $sep . $pname; 89 } 90 } 91 92 $resolver = new PageResolver(getNS($ID)); 93 $this->pagename = $resolver->resolveId($this->replace($this->pagename)); 94 95 if ($this->pagename === '') { 96 throw new Exception($this->getLang('e_pagename')); 97 } 98 } 99 100 /** 101 * Handle templates from addpage field 102 * 103 * @param helper_plugin_bureaucracy_field[] $fields List of field objects 104 * @return array 105 */ 106 function getAdditionalTargetpages($fields) { 107 global $ID; 108 $ns = getNS($ID); 109 110 foreach ($fields as $field) { 111 if (!is_null($field->getParam('page_tpl')) && !is_null($field->getParam('page_tgt')) ) { 112 $resolver = new PageResolver($ns); 113 114 //template 115 $templatepage = $this->replace($field->getParam('page_tpl')); 116 $templatepage = $resolver->resolveId($templatepage); 117 118 //target 119 $relativetargetpage = $resolver->resolveId($field->getParam('page_tgt')); 120 $targetpage = "$this->pagename:$relativetargetpage"; 121 122 $auth = $this->aclcheck($templatepage); // runas 123 if ($auth >= AUTH_READ ) { 124 $this->addParsedTargetpage($targetpage, $templatepage); 125 } 126 } 127 } 128 } 129 130 /** 131 * Returns raw pagetemplate contents for the ID's namespace 132 * 133 * @param string $id the id of the page to be created 134 * @return string raw pagetemplate content 135 */ 136 protected function rawPageTemplate($id) { 137 global $conf; 138 139 $path = dirname(wikiFN($id)); 140 if(file_exists($path.'/_template.txt')) { 141 $tplfile = $path.'/_template.txt'; 142 } else { 143 // search upper namespaces for templates 144 $len = strlen(rtrim($conf['datadir'], '/')); 145 while(strlen($path) >= $len) { 146 if(file_exists($path.'/__template.txt')) { 147 $tplfile = $path.'/__template.txt'; 148 break; 149 } 150 $path = substr($path, 0, strrpos($path, '/')); 151 } 152 } 153 154 $tpl = io_readFile($tplfile); 155 return $tpl; 156 } 157 158 /** 159 * Load template(s) for targetpage as given via action field 160 * 161 * @param string $tpl template name as given in form 162 * @return string parsed templatename 163 */ 164 protected function getActionTargetpages($tpl) { 165 global $USERINFO; 166 global $conf; 167 global $ID; 168 $runas = $this->getConf('runas'); 169 170 if ($tpl == '_') { 171 // use namespace template 172 if (!isset($this->targetpages[$this->pagename])) { 173 $raw = $this->rawPageTemplate($this->pagename); 174 $this->noreplace_save($raw); 175 $this->targetpages[$this->pagename] = pageTemplate(array($this->pagename)); 176 } 177 } elseif ($tpl !== '!') { 178 $tpl = $this->replace($tpl); 179 180 // resolve templates, but keep references to whole namespaces intact (ending in a colon) 181 $resolver = new PageResolver(getNS($ID)); 182 if(substr($tpl, -1) == ':') { 183 $tpl = $tpl.'xxx'; // append a fake page name 184 $tpl = $resolver->resolveId($tpl); 185 $tpl = substr($tpl, 0, -3); // cut off fake page name again 186 } else { 187 $tpl = $resolver->resolveId($tpl); 188 } 189 190 $backup = array(); 191 if ($runas) { 192 // Hack user credentials. 193 $backup = array($_SERVER['REMOTE_USER'], $USERINFO['grps']); 194 $_SERVER['REMOTE_USER'] = $runas; 195 $USERINFO['grps'] = array(); 196 } 197 198 $template_pages = array(); 199 //search checks acl (as runas) 200 $opts = array( 201 'depth' => 0, 202 'listfiles' => true, 203 'showhidden' => true 204 ); 205 search($template_pages, $conf['datadir'], 'search_universal', $opts, str_replace(':', '/', getNS($tpl))); 206 207 foreach ($template_pages as $template_page) { 208 $templatepageid = cleanID($template_page['id']); 209 // try to replace $tpl path with $this->pagename path in the founded $templatepageid 210 // - a single-page template will only match on itself and will be replaced, 211 // other newtargets are pages in same namespace, so aren't changed 212 // - a namespace as template will match at the namespaces-part of the path of pages in this namespace 213 // so these newtargets are changed 214 // if there exist a single-page and a namespace with name $tpl, both are selected 215 $newTargetpageid = preg_replace('/^' . preg_quote_cb(cleanID($tpl)) . '($|:)/', $this->pagename . '$1', $templatepageid); 216 217 if ($newTargetpageid === $templatepageid) { 218 // only a single-page template or page in the namespace template 219 // which matches the $tpl path are changed 220 continue; 221 } 222 223 if (!isset($this->targetpages[$newTargetpageid])) { 224 $this->addParsedTargetpage($newTargetpageid, $templatepageid); 225 } 226 } 227 228 if ($runas) { 229 /* Restore user credentials. */ 230 list($_SERVER['REMOTE_USER'], $USERINFO['grps']) = $backup; 231 } 232 } 233 return $tpl; 234 } 235 236 /** 237 * Checks for existance and access of target pages 238 * 239 * @return mixed 240 * @throws Exception 241 */ 242 protected function checkTargetPageNames() { 243 foreach (array_keys($this->targetpages) as $pname) { 244 // prevent overriding already existing pages 245 if (page_exists($pname)) { 246 throw new Exception(sprintf($this->getLang('e_pageexists'), html_wikilink($pname))); 247 } 248 249 $auth = $this->aclcheck($pname); 250 if ($auth < AUTH_CREATE) { 251 throw new Exception($this->getLang('e_denied')); 252 } 253 } 254 } 255 256 /** 257 * Perform replacements on the collected templates, and save the pages. 258 * 259 * Note: wrt runas, for changelog are used: 260 * - $INFO['userinfo']['name'] 261 * - $INPUT->server->str('REMOTE_USER') 262 */ 263 protected function replaceAndSavePages($fields) { 264 global $ID; 265 foreach ($this->targetpages as $pageName => $template) { 266 // set NSBASE var to make certain dataplugin constructs easier 267 $this->patterns['__nsbase__'] = '/@NSBASE@/'; 268 $this->values['__nsbase__'] = noNS(getNS($pageName)); 269 270 $evdata = array( 271 'patterns' => &$this->patterns, 272 'values' => &$this->values, 273 'id' => $pageName, 274 'template' => $template, 275 'form' => $ID, 276 'fields' => $fields 277 ); 278 279 $event = new Doku_Event('PLUGIN_BUREAUCRACY_TEMPLATE_SAVE', $evdata); 280 if($event->advise_before()) { 281 // save page 282 saveWikiText( 283 $evdata['id'], 284 cleanText($this->replace($evdata['template'], false)), 285 sprintf($this->getLang('summary'), $ID) 286 ); 287 } 288 $event->advise_after(); 289 } 290 } 291 292 /** 293 * (Callback) Sorts first by namespace depth, next by page ids 294 * 295 * @param string $a 296 * @param string $b 297 * @return int positive if $b is in deeper namespace than $a, negative higher. 298 * further sorted by pageids 299 * 300 * return an integer less than, equal to, or 301 * greater than zero if the first argument is considered to be 302 * respectively less than, equal to, or greater than the second. 303 */ 304 public function _sorttargetpages($a, $b) { 305 $ns_diff = substr_count($a, ':') - substr_count($b, ':'); 306 return ($ns_diff === 0) ? strcmp($a, $b) : ($ns_diff > 0 ? -1 : 1); 307 } 308 309 /** 310 * (Callback) Build content of item 311 * 312 * @param array $item 313 * @return string 314 */ 315 public function html_list_index($item){ 316 $ret = ''; 317 if($item['type']=='f'){ 318 $ret .= html_wikilink(':'.$item['id']); 319 } else { 320 $ret .= '<strong>' . trim(substr($item['id'], strrpos($item['id'], ':', -2)), ':') . '</strong>'; 321 } 322 return $ret; 323 } 324 325 /** 326 * Build thanks message, trigger indexing and rendering of new pages. 327 * 328 * @param string $thanks 329 * @return string html of thanks message or when redirect the first page id of created pages 330 */ 331 protected function buildThankYouPage($thanks) { 332 global $ID; 333 $backupID = $ID; 334 335 $html = "<p>$thanks</p>"; 336 337 // Build result tree 338 $pages = array_keys($this->targetpages); 339 usort($pages, array($this, '_sorttargetpages')); 340 341 $data = array(); 342 $last_folder = array(); 343 foreach ($pages as $ID) { 344 $lvl = substr_count($ID, ':'); 345 for ($n = 0; $n < $lvl; ++$n) { 346 if (!isset($last_folder[$n]) || strpos($ID, $last_folder[$n]['id']) !== 0) { 347 $last_folder[$n] = array( 348 'id' => substr($ID, 0, strpos($ID, ':', ($n > 0 ? strlen($last_folder[$n - 1]['id']) : 0) + 1) + 1), 349 'level' => $n + 1, 350 'open' => 1, 351 'type' => null, 352 ); 353 $data[] = $last_folder[$n]; 354 } 355 } 356 $data[] = array('id' => $ID, 'level' => 1 + substr_count($ID, ':'), 'type' => 'f'); 357 } 358 $index = new dokuwiki\Ui\Index(); 359 $html .= html_buildlist($data, 'idx', array($this, 'html_list_index'), array($index, 'tagListItem')); 360 361 // Add indexer bugs for every just-created page 362 $html .= '<div class="no">'; 363 ob_start(); 364 foreach ($pages as $ID) { 365 // indexerWebBug uses ID and INFO[exists], but the bureaucracy form 366 // page always exists, as does the just-saved page, so INFO[exists] 367 // is correct in any case 368 tpl_indexerWebBug(); 369 370 // the iframe will trigger real rendering of the pages to make sure 371 // any used plugins are initialized (eg. the do plugin) 372 echo '<iframe src="' . wl($ID, array('do' => 'export_html')) . '" width="1" height="1" style="visibility:hidden"></iframe>'; 373 } 374 $html .= ob_get_contents(); 375 ob_end_clean(); 376 $html .= '</div>'; 377 378 $ID = $backupID; 379 return $html; 380 } 381 382 /** 383 * move the uploaded files to <pagename>:FILENAME 384 * 385 * 386 * @param helper_plugin_bureaucracy_field[] $fields 387 * @throws Exception 388 */ 389 protected function processUploads($fields) { 390 foreach($fields as $field) { 391 392 if($field->getFieldType() !== 'file') continue; 393 394 $label = $field->getParam('label'); 395 $file = $field->getParam('file'); 396 $ns = $field->getParam('namespace'); 397 398 //skip empty files 399 if(!$file['size']) { 400 $this->values[$label] = ''; 401 continue; 402 } 403 404 $id = $ns.':'.$file['name']; 405 resolve_mediaid($this->pagename, $id, $ignored); // resolve relatives 406 407 $auth = $this->aclcheck($id); // runas 408 $move = 'copy_uploaded_file'; 409 //prevent from is_uploaded_file() check 410 if(defined('DOKU_UNITTEST')) { 411 $move = 'copy'; 412 } 413 $res = media_save( 414 array('name' => $file['tmp_name']), 415 $id, 416 false, 417 $auth, 418 $move); 419 420 if(is_array($res)) throw new Exception($res[0]); 421 422 $this->values[$label] = $res; 423 424 } 425 } 426 427 /** 428 * Load page data and do default pattern replacements like namespace templates do 429 * and add it to list of targetpages 430 * 431 * Note: for runas the values of the real user are used for the placeholders 432 * @NAME@ => $USERINFO['name'] 433 * @MAIL@ => $USERINFO['mail'] 434 * and the replaced value: 435 * @USER@ => $INPUT->server->str('REMOTE_USER') 436 * 437 * @param string $targetpageid pageid of destination 438 * @param string $templatepageid pageid of template for this targetpage 439 */ 440 protected function addParsedTargetpage($targetpageid, $templatepageid) { 441 $tpl = rawWiki($templatepageid); 442 $this->noreplace_save($tpl); 443 444 $data = array( 445 'id' => $targetpageid, 446 'tpl' => $tpl, 447 'doreplace' => true, 448 ); 449 parsePageTemplate($data); 450 451 //collect and apply some other replacements 452 $patterns = array(); 453 $values = array(); 454 $keys = array('__lang__', '__trans__', '__year__', '__month__', '__day__', '__time__'); 455 foreach($keys as $key) { 456 $patterns[$key] = $this->patterns[$key]; 457 $values[$key] = $this->values[$key]; 458 } 459 460 $this->targetpages[$targetpageid] = preg_replace($patterns, $values, $data['tpl']); 461 } 462 463} 464// vim:ts=4:sw=4:et:enc=utf-8: 465