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