* @author Adrian Lang */ // must be run within Dokuwiki use dokuwiki\Utf8\PhpString; if(!defined('DOKU_INC')) die(); /** * All DokuWiki plugins to extend the parser/rendering mechanism * need to inherit from this class */ class syntax_plugin_bureaucracy extends DokuWiki_Syntax_Plugin { private $form_id = 0; var $patterns = array(); var $values = array(); var $noreplace = null; var $functions = array(); /** * Prepare some replacements */ public function __construct() { $this->prepareDateTimereplacements(); $this->prepareNamespacetemplateReplacements(); $this->prepareFunctions(); } /** * What kind of syntax are we? */ public function getType() { return 'substition'; } /** * What about paragraphs? */ public function getPType() { return 'block'; } /** * Where to sort in? */ public function getSort() { return 155; } /** * Connect pattern to lexer * * @param string $mode */ public function connectTo($mode) { $this->Lexer->addSpecialPattern('
.*?
', $mode, 'plugin_bureaucracy'); } /** * Handler to prepare matched data for the rendering process * * @param string $match The text matched by the patterns * @param int $state The lexer state for the match * @param int $pos The character position of the matched text * @param Doku_Handler $handler The Doku_Handler object * @return bool|array Return an array with all data you want to use in render, false don't add an instruction */ public function handle($match, $state, $pos, Doku_Handler $handler) { $match = substr($match, 6, -7); // remove form wrap $lines = explode("\n", $match); $actions = $rawactions = array(); $thanks = ''; $labels = ''; // parse the lines into an command/argument array $cmds = array(); while(count($lines) > 0) { $line = trim(array_shift($lines)); if(!$line) continue; $args = $this->_parse_line($line, $lines); $args[0] = $this->_sanitizeClassName($args[0]); if(in_array($args[0], array('action', 'thanks', 'labels'))) { if(count($args) < 2) { msg(sprintf($this->getLang('e_missingargs'), hsc($args[0]), hsc($args[1])), -1); continue; } // is action element? if($args[0] == 'action') { array_shift($args); $rawactions[] = array('type' => array_shift($args), 'argv' => $args); continue; } // is thank you text? if($args[0] == 'thanks') { $thanks = $args[1]; continue; } // is labels? if($args[0] == 'labels') { $labels = $args[1]; continue; } } if(strpos($args[0], '_') === false) { $name = 'bureaucracy_field' . $args[0]; } else { //name convention: plugin_componentname $name = $args[0]; } /** @var helper_plugin_bureaucracy_field $field */ $field = $this->loadHelper($name, false); if($field && is_a($field, 'helper_plugin_bureaucracy_field')) { $field->initialize($args); $cmds[] = $field; } else { $evdata = array('fields' => &$cmds, 'args' => $args); $event = new Doku_Event('PLUGIN_BUREAUCRACY_FIELD_UNKNOWN', $evdata); if($event->advise_before()) { msg(sprintf($this->getLang('e_unknowntype'), hsc($name)), -1); } } } // check if action is available foreach($rawactions as $action) { $action['type'] = $this->_sanitizeClassName($action['type']); if(strpos($action['type'], '_') === false) { $action['actionname'] = 'bureaucracy_action' . $action['type']; } else { //name convention for other plugins: plugin_componentname $action['actionname'] = $action['type']; } list($plugin, $component) = explode('_', $action['actionname']); $alternativename = $action['type'] . '_'. $action['type']; // bureaucracy_action or _ if(!plugin_isdisabled($action['actionname']) || @file_exists(DOKU_PLUGIN . $plugin . '/helper/' . $component . '.php')) { $actions[] = $action; // shortcut for other plugins with component name _ } elseif(plugin_isdisabled($alternativename) || !@file_exists(DOKU_PLUGIN . $action['type'] . '/helper/' . $action['type'] . '.php')) { $action['actionname'] = $alternativename; $actions[] = $action; // not found } else { $evdata = array('actions' => &$actions, 'action' => $action); $event = new Doku_Event('PLUGIN_BUREAUCRACY_ACTION_UNKNOWN', $evdata); if($event->advise_before()) { msg(sprintf($this->getLang('e_unknownaction'), hsc($action['actionname'])), -1); } } } // action(s) found? if(count($actions) < 1) { msg($this->getLang('e_noaction'), -1); } // set thank you message if(!$thanks) { $thanks = ""; foreach($actions as $action) { $thanks .= $this->getLang($action['type'] . '_thanks'); } } else { $thanks = hsc($thanks); } return array( 'fields' => $cmds, 'actions' => $actions, 'thanks' => $thanks, 'labels' => $labels ); } /** * Handles the actual output creation. * * @param string $format output format being rendered * @param Doku_Renderer $R the current renderer object * @param array $data data created by handler() * @return boolean rendered correctly? (however, returned value is not used at the moment) */ public function render($format, Doku_Renderer $R, $data) { if($format != 'xhtml') return false; $R->info['cache'] = false; // don't cache /** * replace some time and name placeholders in the default values * @var $field helper_plugin_bureaucracy_field */ foreach($data['fields'] as &$field) { if(isset($field->opt['value'])) { $field->opt['value'] = $this->replace($field->opt['value']); } } if($data['labels']) $this->loadlabels($data); $this->form_id++; if(isset($_POST['bureaucracy']) && checkSecurityToken() && $_POST['bureaucracy']['$$id'] == $this->form_id) { $success = $this->_handlepost($data); if($success !== false) { $R->doc .= '
' . $success . '
'; return true; } } $R->doc .= $this->_htmlform($data['fields']); return true; } /** * Initializes the labels, loaded from a defined labelpage * * @param array $data all data passed to render() */ protected function loadlabels(&$data) { global $INFO; $labelpage = $data['labels']; $exists = false; resolve_pageid($INFO['namespace'], $labelpage, $exists); if(!$exists) { msg(sprintf($this->getLang('e_labelpage'), html_wikilink($labelpage)), -1); return; } // parse simple list (first level cdata only) $labels = array(); $instructions = p_cached_instructions(wikiFN($labelpage)); $inli = 0; $item = ''; foreach($instructions as $instruction) { if($instruction[0] == 'listitem_open') { $inli++; continue; } if($inli === 1 && $instruction[0] == 'cdata') { $item .= $instruction[1][0]; } if($instruction[0] == 'listitem_close') { $inli--; if($inli === 0) { list($k, $v) = explode('=', $item, 2); $k = trim($k); $v = trim($v); if($k && $v) $labels[$k] = $v; $item = ''; } } } // apply labels to all fields $len = count($data['fields']); for($i = 0; $i < $len; $i++) { if(isset($data['fields'][$i]->depends_on)) { // translate dependency on fieldsets $label = $data['fields'][$i]->depends_on[0]; if(isset($labels[$label])) { $data['fields'][$i]->depends_on[0] = $labels[$label]; } } else if(isset($data['fields'][$i]->opt['label'])) { // translate field labels $label = $data['fields'][$i]->opt['label']; if(isset($labels[$label])) { $data['fields'][$i]->opt['display'] = $labels[$label]; } } } if(isset($data['thanks'])) { if(isset($labels[$data['thanks']])) { $data['thanks'] = $labels[$data['thanks']]; } } } /** * Validate posted data, perform action(s) * * @param array $data all data passed to render() * @return bool|string * returns thanks message when fields validated and performed the action(s) succesfully; * otherwise returns false. */ private function _handlepost($data) { $success = true; foreach($data['fields'] as $index => $field) { /** @var $field helper_plugin_bureaucracy_field */ $isValid = true; if($field->getFieldType() === 'file') { $file = array(); foreach($_FILES['bureaucracy'] as $key => $value) { $file[$key] = $value[$index]; } $isValid = $field->handle_post($file, $data['fields'], $index, $this->form_id); } elseif($field->getFieldType() === 'fieldset' || !$field->hidden) { $isValid = $field->handle_post($_POST['bureaucracy'][$index] ?? null, $data['fields'], $index, $this->form_id); } if(!$isValid) { // Do not return instantly to allow validation of all fields. $success = false; } } if(!$success) { return false; } $thanks_array = array(); foreach($data['actions'] as $actionData) { /** @var helper_plugin_bureaucracy_action $action */ $action = $this->loadHelper($actionData['actionname'], false); // action helper found? if(!$action) { msg(sprintf($this->getLang('e_unknownaction'), hsc($actionData['actionname'])), -1); return false; } try { $thanks_array[] = $action->run( $data['fields'], $data['thanks'], $actionData['argv'] ); } catch(Exception $e) { msg($e->getMessage(), -1); return false; } } // Perform after_action hooks foreach($data['fields'] as $field) { $field->after_action(); } // create thanks string $thanks = implode('', array_unique($thanks_array)); return $thanks; } /** * Create the form * * @param helper_plugin_bureaucracy_field[] $fields array with form fields * @return string html of the form */ private function _htmlform($fields) { global $INFO; $form = new Doku_Form(array('class' => 'bureaucracy__plugin', 'id' => 'bureaucracy__plugin' . $this->form_id, 'enctype' => 'multipart/form-data')); $form->addHidden('id', $INFO['id']); $form->addHidden('bureaucracy[$$id]', $this->form_id); foreach($fields as $id => $field) { $field->renderfield(array('name' => 'bureaucracy[' . $id . ']'), $form, $this->form_id); } return $form->getForm(); } /** * Parse a line into (quoted) arguments * Splits line at spaces, except when quoted * * @author William Fletcher * * @param string $line line to parse * @param array $lines all remaining lines * @return array with all the arguments */ private function _parse_line($line, &$lines) { $args = array(); $inQuote = false; $escapedQuote = false; $arg = ''; do { $len = strlen($line); for($i = 0; $i < $len; $i++) { if($line[$i] == '"') { if($inQuote) { if($escapedQuote) { $arg .= '"'; $escapedQuote = false; continue; } if($i + 1 < $len && $line[$i + 1] == '"') { $escapedQuote = true; continue; } array_push($args, $arg); $inQuote = false; $arg = ''; continue; } else { $inQuote = true; continue; } } else if($line[$i] == ' ') { if($inQuote) { $arg .= ' '; continue; } else { if(strlen($arg) < 1) continue; array_push($args, $arg); $arg = ''; continue; } } $arg .= $line[$i]; } if(!$inQuote || count($lines) === 0) break; $line = array_shift($lines); $arg .= "\n"; } while(true); if(strlen($arg) > 0) array_push($args, $arg); return $args; } /** * Clean class name * * @param string $classname * @return string cleaned name */ private function _sanitizeClassName($classname) { return preg_replace('/[^\w\x7f-\xff]/', '', strtolower($classname)); } /** * Save content in tags into $this->noreplace * * @param string $input The text to work on */ protected function noreplace_save($input) { $pattern = '/(.*?)<\/noreplace>/is'; //save content of tags preg_match_all($pattern, $input, $matches); $this->noreplace = $matches[1]; } /** * Apply replacement patterns and values as prepared earlier * (disable $strftime to prevent double replacements with default strftime() replacements in nstemplate) * * @param string $input The text to work on * @param bool $strftime Apply strftime() replacements * @return string processed text */ function replace($input, $strftime = true) { //in helper_plugin_struct_field::setVal $input can be an array //just return $input in that case if (!is_string($input)) return $input; if (is_null($this->noreplace)) $this->noreplace_save($input); foreach ($this->values as $label => $value) { $pattern = $this->patterns[$label]; if (is_callable($value)) { $input = preg_replace_callback( $pattern, $value, $input ); } else { $input = preg_replace($pattern, $value, $input); } } if($strftime) { $input = preg_replace_callback( '/%./', function($m){return strftime($m[0]);}, $input ); } // user syntax: %%.(.*?) // strftime() is already applied once, so syntax is at this point: %.(.*?) $input = preg_replace_callback( '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', array($this, 'replacedate'), $input ); //run functions foreach ($this->functions as $name => $callback) { $pattern = '/@' . preg_quote($name) . '\((.*?)\)@/'; if (is_callable($callback)) { $input = preg_replace_callback($pattern, function ($matches) use ($callback) { return call_user_func($callback, $matches[1]); }, $input); } } //replace tags with their original content $pattern = '/.*?<\/noreplace>/is'; if (is_array($this->noreplace)) foreach ($this->noreplace as $nr) { $input = preg_replace($pattern, $nr, $input, 1); } return $input; } /** * (callback) Replace date by request datestring * e.g. '%m(30-11-1975)' is replaced by '11' * * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern * @return string */ function replacedate($match) { global $conf; //no 2nd argument for default date format $match[2] = $match[2] ?? $conf['dformat']; return strftime($match[2], strtotime($match[1])); } /** * Same replacements as applied at template namespaces * * @see parsePageTemplate() */ function prepareNamespacetemplateReplacements() { /* @var Input $INPUT */ global $INPUT; global $INFO; global $USERINFO; global $conf; global $ID; $this->patterns['__formpage_id__'] = '/@FORMPAGE_ID@/'; $this->patterns['__formpage_ns__'] = '/@FORMPAGE_NS@/'; $this->patterns['__formpage_curns__'] = '/@FORMPAGE_CURNS@/'; $this->patterns['__formpage_file__'] = '/@FORMPAGE_FILE@/'; $this->patterns['__formpage_!file__'] = '/@FORMPAGE_!FILE@/'; $this->patterns['__formpage_!file!__'] = '/@FORMPAGE_!FILE!@/'; $this->patterns['__formpage_page__'] = '/@FORMPAGE_PAGE@/'; $this->patterns['__formpage_!page__'] = '/@FORMPAGE_!PAGE@/'; $this->patterns['__formpage_!!page__'] = '/@FORMPAGE_!!PAGE@/'; $this->patterns['__formpage_!page!__'] = '/@FORMPAGE_!PAGE!@/'; $this->patterns['__user__'] = '/@USER@/'; $this->patterns['__name__'] = '/@NAME@/'; $this->patterns['__mail__'] = '/@MAIL@/'; $this->patterns['__date__'] = '/@DATE@/'; // replace placeholders $localid = isset($INFO['id']) ? $INFO['id'] : $ID; $file = noNS($localid); $page = strtr($file, $conf['sepchar'], ' '); $this->values['__formpage_id__'] = $localid; $this->values['__formpage_ns__'] = getNS($localid); $this->values['__formpage_curns__'] = curNS($localid); $this->values['__formpage_file__'] = $file; $this->values['__formpage_!file__'] = PhpString::ucfirst($file); $this->values['__formpage_!file!__'] = PhpString::strtoupper($file); $this->values['__formpage_page__'] = $page; $this->values['__formpage_!page__'] = PhpString::ucfirst($page); $this->values['__formpage_!!page__'] = PhpString::ucwords($page); $this->values['__formpage_!page!__'] = PhpString::strtoupper($page); $this->values['__user__'] = $INPUT->server->str('REMOTE_USER'); $this->values['__name__'] = $USERINFO['name'] ?? ''; $this->values['__mail__'] = $USERINFO['mail'] ?? ''; $this->values['__date__'] = strftime($conf['dformat']); } /** * Date time replacements */ function prepareDateTimereplacements() { $this->patterns['__year__'] = '/@YEAR@/'; $this->patterns['__month__'] = '/@MONTH@/'; $this->patterns['__monthname__'] = '/@MONTHNAME@/'; $this->patterns['__day__'] = '/@DAY@/'; $this->patterns['__time__'] = '/@TIME@/'; $this->patterns['__timesec__'] = '/@TIMESEC@/'; $this->values['__year__'] = date('Y'); $this->values['__month__'] = date('m'); $this->values['__monthname__'] = date('B'); $this->values['__day__'] = date('d'); $this->values['__time__'] = date('H:i'); $this->values['__timesec__'] = date('H:i:s'); } /** * Functions that can be used after replacements */ function prepareFunctions() { $this->functions['curNS'] = 'curNS'; $this->functions['getNS'] = 'getNS'; $this->functions['noNS'] = 'noNS'; $this->functions['p_get_first_heading'] = 'p_get_first_heading'; } }