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