1<?php 2/** 3 * Bureaucracy Plugin: Allows flexible creation of forms 4 * 5 * This plugin allows definition of forms in wiki pages. The forms can be 6 * submitted via email or used to create new pages from templates. 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Andreas Gohr <andi@splitbrain.org> 10 * @author Adrian Lang <dokuwiki@cosmocode.de> 11 */ 12// must be run within Dokuwiki 13use dokuwiki\Utf8\PhpString; 14 15if(!defined('DOKU_INC')) die(); 16 17/** 18 * All DokuWiki plugins to extend the parser/rendering mechanism 19 * need to inherit from this class 20 */ 21class syntax_plugin_bureaucracy extends DokuWiki_Syntax_Plugin { 22 23 private $form_id = 0; 24 var $patterns = array(); 25 var $values = array(); 26 var $noreplace = null; 27 var $functions = array(); 28 29 /** 30 * Prepare some replacements 31 */ 32 public function __construct() { 33 $this->prepareDateTimereplacements(); 34 $this->prepareNamespacetemplateReplacements(); 35 $this->prepareFunctions(); 36 } 37 38 /** 39 * What kind of syntax are we? 40 */ 41 public function getType() { 42 return 'substition'; 43 } 44 45 /** 46 * What about paragraphs? 47 */ 48 public function getPType() { 49 return 'block'; 50 } 51 52 /** 53 * Where to sort in? 54 */ 55 public function getSort() { 56 return 155; 57 } 58 59 /** 60 * Connect pattern to lexer 61 * 62 * @param string $mode 63 */ 64 public function connectTo($mode) { 65 $this->Lexer->addSpecialPattern('<form>.*?</form>', $mode, 'plugin_bureaucracy'); 66 } 67 68 /** 69 * Handler to prepare matched data for the rendering process 70 * 71 * @param string $match The text matched by the patterns 72 * @param int $state The lexer state for the match 73 * @param int $pos The character position of the matched text 74 * @param Doku_Handler $handler The Doku_Handler object 75 * @return bool|array Return an array with all data you want to use in render, false don't add an instruction 76 */ 77 public function handle($match, $state, $pos, Doku_Handler $handler) { 78 $match = substr($match, 6, -7); // remove form wrap 79 $lines = explode("\n", $match); 80 $actions = $rawactions = array(); 81 $thanks = ''; 82 $labels = ''; 83 84 // parse the lines into an command/argument array 85 $cmds = array(); 86 while(count($lines) > 0) { 87 $line = trim(array_shift($lines)); 88 if(!$line) continue; 89 $args = $this->_parse_line($line, $lines); 90 $args[0] = $this->_sanitizeClassName($args[0]); 91 92 if(in_array($args[0], array('action', 'thanks', 'labels'))) { 93 if(count($args) < 2) { 94 msg(sprintf($this->getLang('e_missingargs'), hsc($args[0]), hsc($args[1])), -1); 95 continue; 96 } 97 98 // is action element? 99 if($args[0] == 'action') { 100 array_shift($args); 101 $rawactions[] = array('type' => array_shift($args), 'argv' => $args); 102 continue; 103 } 104 105 // is thank you text? 106 if($args[0] == 'thanks') { 107 $thanks = $args[1]; 108 continue; 109 } 110 111 // is labels? 112 if($args[0] == 'labels') { 113 $labels = $args[1]; 114 continue; 115 } 116 } 117 118 if(strpos($args[0], '_') === false) { 119 $name = 'bureaucracy_field' . $args[0]; 120 } else { 121 //name convention: plugin_componentname 122 $name = $args[0]; 123 } 124 125 /** @var helper_plugin_bureaucracy_field $field */ 126 $field = $this->loadHelper($name, false); 127 if($field && is_a($field, 'helper_plugin_bureaucracy_field')) { 128 $field->initialize($args); 129 $cmds[] = $field; 130 } else { 131 $evdata = array('fields' => &$cmds, 'args' => $args); 132 $event = new Doku_Event('PLUGIN_BUREAUCRACY_FIELD_UNKNOWN', $evdata); 133 if($event->advise_before()) { 134 msg(sprintf($this->getLang('e_unknowntype'), hsc($name)), -1); 135 } 136 } 137 138 } 139 140 // check if action is available 141 foreach($rawactions as $action) { 142 $action['type'] = $this->_sanitizeClassName($action['type']); 143 144 if(strpos($action['type'], '_') === false) { 145 $action['actionname'] = 'bureaucracy_action' . $action['type']; 146 } else { 147 //name convention for other plugins: plugin_componentname 148 $action['actionname'] = $action['type']; 149 } 150 151 list($plugin, $component) = explode('_', $action['actionname']); 152 $alternativename = $action['type'] . '_'. $action['type']; 153 154 // bureaucracy_action<name> or <plugin>_<componentname> 155 if(!plugin_isdisabled($action['actionname']) || @file_exists(DOKU_PLUGIN . $plugin . '/helper/' . $component . '.php')) { 156 $actions[] = $action; 157 158 // shortcut for other plugins with component name <name>_<name> 159 } elseif(plugin_isdisabled($alternativename) || !@file_exists(DOKU_PLUGIN . $action['type'] . '/helper/' . $action['type'] . '.php')) { 160 $action['actionname'] = $alternativename; 161 $actions[] = $action; 162 163 // not found 164 } else { 165 $evdata = array('actions' => &$actions, 'action' => $action); 166 $event = new Doku_Event('PLUGIN_BUREAUCRACY_ACTION_UNKNOWN', $evdata); 167 if($event->advise_before()) { 168 msg(sprintf($this->getLang('e_unknownaction'), hsc($action['actionname'])), -1); 169 } 170 } 171 } 172 173 // action(s) found? 174 if(count($actions) < 1) { 175 msg($this->getLang('e_noaction'), -1); 176 } 177 178 // set thank you message 179 if(!$thanks) { 180 $thanks = ""; 181 foreach($actions as $action) { 182 $thanks .= $this->getLang($action['type'] . '_thanks'); 183 } 184 } else { 185 $thanks = hsc($thanks); 186 } 187 return array( 188 'fields' => $cmds, 189 'actions' => $actions, 190 'thanks' => $thanks, 191 'labels' => $labels 192 ); 193 } 194 195 /** 196 * Handles the actual output creation. 197 * 198 * @param string $format output format being rendered 199 * @param Doku_Renderer $R the current renderer object 200 * @param array $data data created by handler() 201 * @return boolean rendered correctly? (however, returned value is not used at the moment) 202 */ 203 public function render($format, Doku_Renderer $R, $data) { 204 if($format != 'xhtml') return false; 205 $R->info['cache'] = false; // don't cache 206 207 /** 208 * replace some time and name placeholders in the default values 209 * @var $field helper_plugin_bureaucracy_field 210 */ 211 foreach($data['fields'] as &$field) { 212 if(isset($field->opt['value'])) { 213 $field->opt['value'] = $this->replace($field->opt['value']); 214 } 215 } 216 217 if($data['labels']) $this->loadlabels($data); 218 219 $this->form_id++; 220 if(isset($_POST['bureaucracy']) && checkSecurityToken() && $_POST['bureaucracy']['$$id'] == $this->form_id) { 221 $success = $this->_handlepost($data); 222 if($success !== false) { 223 $R->doc .= '<div class="bureaucracy__plugin" id="scroll__here">' . $success . '</div>'; 224 return true; 225 } 226 } 227 228 $R->doc .= $this->_htmlform($data['fields']); 229 230 return true; 231 } 232 233 /** 234 * Initializes the labels, loaded from a defined labelpage 235 * 236 * @param array $data all data passed to render() 237 */ 238 protected function loadlabels(&$data) { 239 global $INFO; 240 $labelpage = $data['labels']; 241 $exists = false; 242 resolve_pageid($INFO['namespace'], $labelpage, $exists); 243 if(!$exists) { 244 msg(sprintf($this->getLang('e_labelpage'), html_wikilink($labelpage)), -1); 245 return; 246 } 247 248 // parse simple list (first level cdata only) 249 $labels = array(); 250 $instructions = p_cached_instructions(wikiFN($labelpage)); 251 $inli = 0; 252 $item = ''; 253 foreach($instructions as $instruction) { 254 if($instruction[0] == 'listitem_open') { 255 $inli++; 256 continue; 257 } 258 if($inli === 1 && $instruction[0] == 'cdata') { 259 $item .= $instruction[1][0]; 260 } 261 if($instruction[0] == 'listitem_close') { 262 $inli--; 263 if($inli === 0) { 264 list($k, $v) = explode('=', $item, 2); 265 $k = trim($k); 266 $v = trim($v); 267 if($k && $v) $labels[$k] = $v; 268 $item = ''; 269 } 270 } 271 } 272 273 // apply labels to all fields 274 $len = count($data['fields']); 275 for($i = 0; $i < $len; $i++) { 276 if(isset($data['fields'][$i]->depends_on)) { 277 // translate dependency on fieldsets 278 $label = $data['fields'][$i]->depends_on[0]; 279 if(isset($labels[$label])) { 280 $data['fields'][$i]->depends_on[0] = $labels[$label]; 281 } 282 283 } else if(isset($data['fields'][$i]->opt['label'])) { 284 // translate field labels 285 $label = $data['fields'][$i]->opt['label']; 286 if(isset($labels[$label])) { 287 $data['fields'][$i]->opt['display'] = $labels[$label]; 288 } 289 } 290 } 291 292 if(isset($data['thanks'])) { 293 if(isset($labels[$data['thanks']])) { 294 $data['thanks'] = $labels[$data['thanks']]; 295 } 296 } 297 298 } 299 300 /** 301 * Validate posted data, perform action(s) 302 * 303 * @param array $data all data passed to render() 304 * @return bool|string 305 * returns thanks message when fields validated and performed the action(s) succesfully; 306 * otherwise returns false. 307 */ 308 private function _handlepost($data) { 309 $success = true; 310 foreach($data['fields'] as $index => $field) { 311 /** @var $field helper_plugin_bureaucracy_field */ 312 313 $isValid = true; 314 if($field->getFieldType() === 'file') { 315 $file = array(); 316 foreach($_FILES['bureaucracy'] as $key => $value) { 317 $file[$key] = $value[$index]; 318 } 319 $isValid = $field->handle_post($file, $data['fields'], $index, $this->form_id); 320 321 } elseif($field->getFieldType() === 'fieldset' || !$field->hidden) { 322 $isValid = $field->handle_post($_POST['bureaucracy'][$index] ?? null, $data['fields'], $index, $this->form_id); 323 } 324 325 if(!$isValid) { 326 // Do not return instantly to allow validation of all fields. 327 $success = false; 328 } 329 } 330 if(!$success) { 331 return false; 332 } 333 334 $thanks_array = array(); 335 336 foreach($data['actions'] as $actionData) { 337 /** @var helper_plugin_bureaucracy_action $action */ 338 $action = $this->loadHelper($actionData['actionname'], false); 339 340 // action helper found? 341 if(!$action) { 342 msg(sprintf($this->getLang('e_unknownaction'), hsc($actionData['actionname'])), -1); 343 return false; 344 } 345 346 try { 347 $thanks_array[] = $action->run( 348 $data['fields'], 349 $data['thanks'], 350 $actionData['argv'] 351 ); 352 } catch(Exception $e) { 353 msg($e->getMessage(), -1); 354 return false; 355 } 356 } 357 358 // Perform after_action hooks 359 foreach($data['fields'] as $field) { 360 $field->after_action(); 361 } 362 363 // create thanks string 364 $thanks = implode('', array_unique($thanks_array)); 365 366 return $thanks; 367 } 368 369 /** 370 * Create the form 371 * 372 * @param helper_plugin_bureaucracy_field[] $fields array with form fields 373 * @return string html of the form 374 */ 375 private function _htmlform($fields) { 376 global $INFO; 377 378 $form = new Doku_Form(array('class' => 'bureaucracy__plugin', 379 'id' => 'bureaucracy__plugin' . $this->form_id, 380 'enctype' => 'multipart/form-data')); 381 $form->addHidden('id', $INFO['id']); 382 $form->addHidden('bureaucracy[$$id]', $this->form_id); 383 384 foreach($fields as $id => $field) { 385 $field->renderfield(array('name' => 'bureaucracy[' . $id . ']'), $form, $this->form_id); 386 } 387 388 return $form->getForm(); 389 } 390 391 /** 392 * Parse a line into (quoted) arguments 393 * Splits line at spaces, except when quoted 394 * 395 * @author William Fletcher <wfletcher@applestone.co.za> 396 * 397 * @param string $line line to parse 398 * @param array $lines all remaining lines 399 * @return array with all the arguments 400 */ 401 private function _parse_line($line, &$lines) { 402 $args = array(); 403 $inQuote = false; 404 $escapedQuote = false; 405 $arg = ''; 406 do { 407 $len = strlen($line); 408 for($i = 0; $i < $len; $i++) { 409 if($line[$i] == '"') { 410 if($inQuote) { 411 if($escapedQuote) { 412 $arg .= '"'; 413 $escapedQuote = false; 414 continue; 415 } 416 if($i + 1 < $len && $line[$i + 1] == '"') { 417 $escapedQuote = true; 418 continue; 419 } 420 array_push($args, $arg); 421 $inQuote = false; 422 $arg = ''; 423 continue; 424 } else { 425 $inQuote = true; 426 continue; 427 } 428 } else if($line[$i] == ' ') { 429 if($inQuote) { 430 $arg .= ' '; 431 continue; 432 } else { 433 if(strlen($arg) < 1) continue; 434 array_push($args, $arg); 435 $arg = ''; 436 continue; 437 } 438 } 439 $arg .= $line[$i]; 440 } 441 if(!$inQuote || count($lines) === 0) break; 442 $line = array_shift($lines); 443 $arg .= "\n"; 444 } while(true); 445 if(strlen($arg) > 0) array_push($args, $arg); 446 return $args; 447 } 448 449 /** 450 * Clean class name 451 * 452 * @param string $classname 453 * @return string cleaned name 454 */ 455 private function _sanitizeClassName($classname) { 456 return preg_replace('/[^\w\x7f-\xff]/', '', strtolower($classname)); 457 } 458 459 /** 460 * Save content in <noreplace> tags into $this->noreplace 461 * 462 * @param string $input The text to work on 463 */ 464 protected function noreplace_save($input) { 465 $pattern = '/<noreplace>(.*?)<\/noreplace>/is'; 466 //save content of <noreplace> tags 467 preg_match_all($pattern, $input, $matches); 468 $this->noreplace = $matches[1]; 469 } 470 471 /** 472 * Apply replacement patterns and values as prepared earlier 473 * (disable $strftime to prevent double replacements with default strftime() replacements in nstemplate) 474 * 475 * @param string $input The text to work on 476 * @param bool $strftime Apply strftime() replacements 477 * @return string processed text 478 */ 479 function replace($input, $strftime = true) { 480 //in helper_plugin_struct_field::setVal $input can be an array 481 //just return $input in that case 482 if (!is_string($input)) return $input; 483 if (is_null($this->noreplace)) $this->noreplace_save($input); 484 485 foreach ($this->values as $label => $value) { 486 $pattern = $this->patterns[$label]; 487 if (is_callable($value)) { 488 $input = preg_replace_callback( 489 $pattern, 490 $value, 491 $input 492 ); 493 } else { 494 $input = preg_replace($pattern, $value, $input); 495 } 496 497 } 498 499 if($strftime) { 500 $input = preg_replace_callback( 501 '/%./', 502 function($m){return strftime($m[0]);}, 503 $input 504 ); 505 } 506 // user syntax: %%.(.*?) 507 // strftime() is already applied once, so syntax is at this point: %.(.*?) 508 $input = preg_replace_callback( 509 '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', 510 array($this, 'replacedate'), 511 $input 512 ); 513 514 //run functions 515 foreach ($this->functions as $name => $callback) { 516 $pattern = '/@' . preg_quote($name) . '\((.*?)\)@/'; 517 if (is_callable($callback)) { 518 $input = preg_replace_callback($pattern, function ($matches) use ($callback) { 519 return call_user_func($callback, $matches[1]); 520 }, $input); 521 } 522 } 523 524 //replace <noreplace> tags with their original content 525 $pattern = '/<noreplace>.*?<\/noreplace>/is'; 526 if (is_array($this->noreplace)) foreach ($this->noreplace as $nr) { 527 $input = preg_replace($pattern, $nr, $input, 1); 528 } 529 530 return $input; 531 } 532 533 /** 534 * (callback) Replace date by request datestring 535 * e.g. '%m(30-11-1975)' is replaced by '11' 536 * 537 * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern 538 * @return string 539 */ 540 function replacedate($match) { 541 global $conf; 542 543 //no 2nd argument for default date format 544 $match[2] = $match[2] ?? $conf['dformat']; 545 546 return strftime($match[2], strtotime($match[1])); 547 } 548 549 /** 550 * Same replacements as applied at template namespaces 551 * 552 * @see parsePageTemplate() 553 */ 554 function prepareNamespacetemplateReplacements() { 555 /* @var Input $INPUT */ 556 global $INPUT; 557 global $INFO; 558 global $USERINFO; 559 global $conf; 560 global $ID; 561 562 $this->patterns['__formpage_id__'] = '/@FORMPAGE_ID@/'; 563 $this->patterns['__formpage_ns__'] = '/@FORMPAGE_NS@/'; 564 $this->patterns['__formpage_curns__'] = '/@FORMPAGE_CURNS@/'; 565 $this->patterns['__formpage_file__'] = '/@FORMPAGE_FILE@/'; 566 $this->patterns['__formpage_!file__'] = '/@FORMPAGE_!FILE@/'; 567 $this->patterns['__formpage_!file!__'] = '/@FORMPAGE_!FILE!@/'; 568 $this->patterns['__formpage_page__'] = '/@FORMPAGE_PAGE@/'; 569 $this->patterns['__formpage_!page__'] = '/@FORMPAGE_!PAGE@/'; 570 $this->patterns['__formpage_!!page__'] = '/@FORMPAGE_!!PAGE@/'; 571 $this->patterns['__formpage_!page!__'] = '/@FORMPAGE_!PAGE!@/'; 572 $this->patterns['__user__'] = '/@USER@/'; 573 $this->patterns['__name__'] = '/@NAME@/'; 574 $this->patterns['__mail__'] = '/@MAIL@/'; 575 $this->patterns['__date__'] = '/@DATE@/'; 576 577 // replace placeholders 578 $localid = isset($INFO['id']) ? $INFO['id'] : $ID; 579 $file = noNS($localid); 580 $page = strtr($file, $conf['sepchar'], ' '); 581 $this->values['__formpage_id__'] = $localid; 582 $this->values['__formpage_ns__'] = getNS($localid); 583 $this->values['__formpage_curns__'] = curNS($localid); 584 $this->values['__formpage_file__'] = $file; 585 $this->values['__formpage_!file__'] = PhpString::ucfirst($file); 586 $this->values['__formpage_!file!__'] = PhpString::strtoupper($file); 587 $this->values['__formpage_page__'] = $page; 588 $this->values['__formpage_!page__'] = PhpString::ucfirst($page); 589 $this->values['__formpage_!!page__'] = PhpString::ucwords($page); 590 $this->values['__formpage_!page!__'] = PhpString::strtoupper($page); 591 $this->values['__user__'] = $INPUT->server->str('REMOTE_USER'); 592 $this->values['__name__'] = $USERINFO['name'] ?? ''; 593 $this->values['__mail__'] = $USERINFO['mail'] ?? ''; 594 $this->values['__date__'] = strftime($conf['dformat']); 595 } 596 597 /** 598 * Date time replacements 599 */ 600 function prepareDateTimereplacements() { 601 $this->patterns['__year__'] = '/@YEAR@/'; 602 $this->patterns['__month__'] = '/@MONTH@/'; 603 $this->patterns['__monthname__'] = '/@MONTHNAME@/'; 604 $this->patterns['__day__'] = '/@DAY@/'; 605 $this->patterns['__time__'] = '/@TIME@/'; 606 $this->patterns['__timesec__'] = '/@TIMESEC@/'; 607 $this->values['__year__'] = date('Y'); 608 $this->values['__month__'] = date('m'); 609 $this->values['__monthname__'] = date('B'); 610 $this->values['__day__'] = date('d'); 611 $this->values['__time__'] = date('H:i'); 612 $this->values['__timesec__'] = date('H:i:s'); 613 614 } 615 616 /** 617 * Functions that can be used after replacements 618 */ 619 function prepareFunctions() { 620 $this->functions['curNS'] = 'curNS'; 621 $this->functions['getNS'] = 'getNS'; 622 $this->functions['noNS'] = 'noNS'; 623 $this->functions['p_get_first_heading'] = 'p_get_first_heading'; 624 } 625} 626