You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

626 lines
21 KiB

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