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