*/ use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\plugin\data\Form\DropdownElement; use dokuwiki\Form\InputElement; use dokuwiki\Form\CheckableElement; use dokuwiki\Form\Element; use dokuwiki\Utf8\PhpString; /** * Class syntax_plugin_data_entry */ class syntax_plugin_data_entry extends SyntaxPlugin { /** * @var helper_plugin_data will hold the data helper plugin */ public $dthlp; /** * Constructor. Load helper plugin */ public function __construct() { $this->dthlp = plugin_load('helper', 'data'); if (!$this->dthlp) msg('Loading the data helper failed. Make sure the data plugin is installed.', -1); } /** * 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 */ public function connectTo($mode) { $this->Lexer->addSpecialPattern( '----+ *dataentry(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+', $mode, 'plugin_data_entry' ); } /** * Handle the match - parse the data * * @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) { if (!$this->dthlp->ready()) return null; // get lines $lines = explode("\n", $match); array_pop($lines); $class = array_shift($lines); $class = str_replace('dataentry', '', $class); $class = trim($class, '- '); // parse info $data = []; $columns = []; foreach ($lines as $line) { // ignore comments preg_match('/^(.*?(?dthlp->column($line[0]); if (isset($matches[2])) { $column['comment'] = $matches[2]; } if ($column['multi']) { if (!isset($data[$column['key']])) { // init with empty array // Note that multiple occurrences of the field are // practically merged $data[$column['key']] = []; } $vals = explode(',', $line[1]); foreach ($vals as $val) { $val = trim($this->dthlp->cleanData($val, $column['type'])); if ($val == '') continue; if (!in_array($val, $data[$column['key']])) { $data[$column['key']][] = $val; } } } else { $data[$column['key']] = $this->dthlp->cleanData($line[1] ?? '', $column['type']); } $columns[$column['key']] = $column; } return [ 'data' => $data, 'cols' => $columns, 'classes' => $class, 'pos' => $pos, 'len' => strlen($match) // not utf8_strlen ]; } /** * Create output or save the data * * @param $format string output format being rendered * @param $renderer Doku_Renderer the current renderer object * @param $data array data created by handler() * @return boolean rendered correctly? */ public function render($format, Doku_Renderer $renderer, $data) { if (is_null($data)) return false; if (!$this->dthlp->ready()) return false; global $ID; switch ($format) { case 'xhtml': /** @var $renderer Doku_Renderer_xhtml */ $this->showData($data, $renderer); return true; case 'metadata': /** @var $renderer Doku_Renderer_metadata */ $this->saveData($data, $ID, $renderer->meta['title'] ?? ''); return true; case 'plugin_data_edit': /** @var $renderer Doku_Renderer_plugin_data_edit */ if (is_a($renderer->form, 'Doku_Form')) { $this->editDataLegacy($data, $renderer); } else { $this->editData($data, $renderer); } return true; default: return false; } } /** * Output the data in a table * * @param array $data * @param Doku_Renderer_xhtml $R */ public function showData($data, $R) { global $ID; $ret = ''; $sectionEditData = ['target' => 'plugin_data', 'hid' => 'data_entry']; $data['classes'] .= ' ' . $R->startSectionEdit($data['pos'], $sectionEditData); $ret .= '
'; $class_names = []; foreach ($data['data'] as $key => $val) { if ($val == '' || is_null($val) || (is_array($val) && count($val) == 0)) continue; $type = $data['cols'][$key]['type']; if (is_array($type)) { $type = $type['type']; } if ($type === 'hidden') continue; $class_name = hsc(sectionID($key, $class_names)); $ret .= '
' . hsc($data['cols'][$key]['title']) . ':
'; $ret .= '
'; if (is_array($val)) { $cnt = count($val); for ($i = 0; $i < $cnt; $i++) { if ($type === 'wiki') { $val[$i] = $ID . '|' . $val[$i]; } $ret .= $this->dthlp->formatData($data['cols'][$key], $val[$i], $R); if ($i < $cnt - 1) { $ret .= ', '; } } } else { if ($type === 'wiki') { $val = $ID . '|' . $val; } $ret .= $this->dthlp->formatData($data['cols'][$key], $val, $R); } $ret .= '
'; } $ret .= '
'; $R->doc .= $ret; $R->finishSectionEdit($data['len'] + $data['pos']); } /** * Save date to the database */ public function saveData($data, $id, $title) { $sqlite = $this->dthlp->getDB(); if (!$sqlite) return false; if (!$title) { $title = $id; } $class = $data['classes']; $sqlite->getPdo()->beginTransaction(); try { // store page info $this->replaceQuery( "INSERT OR IGNORE INTO pages (page,title,class) VALUES (?,?,?)", $id, $title, $class ); // Update title if insert failed (record already saved before) $revision = filemtime(wikiFN($id)); $this->replaceQuery( "UPDATE pages SET title = ?, class = ?, lastmod = ? WHERE page = ?", $title, $class, $revision, $id ); // fetch page id /** @var PDOStatement $res */ $res = $this->replaceQuery("SELECT pid FROM pages WHERE page = ?", $id); $all = $res->fetchAll(\PDO::FETCH_ASSOC); $res->closeCursor(); $pid = (int)$all[0]['pid']; if (!$pid) { throw new Exception("data plugin: failed saving data"); } // remove old data $sqlite->query("DELETE FROM DATA WHERE pid = ?", $pid); // insert new data foreach ($data['data'] as $key => $val) { if (is_array($val)) foreach ($val as $v) { $this->replaceQuery( "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)", $pid, $key, $v ); } else { $this->replaceQuery( "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)", $pid, $key, $val ); } } // finish transaction $sqlite->getPdo()->commit(); } catch (\Exception $exception) { $sqlite->getPdo()->rollBack(); msg(hsc($exception->getMessage()), -1); } return true; } /** * * @fixme replace this madness * @return bool|mixed */ public function replaceQuery() { $args = func_get_args(); $argc = func_num_args(); if ($argc > 1) { for ($i = 1; $i < $argc; $i++) { $data = []; $data['sql'] = $args[$i]; $this->dthlp->replacePlaceholdersInSQL($data); $args[$i] = $data['sql']; } } $sqlite = $this->dthlp->getDB(); if (!$sqlite) return false; return call_user_func_array(array(&$sqlite, 'query'), $args); } /** * The custom editor for editing data entries * * Gets called from action_plugin_data::_editform() where also the form member is attached * * @param array $data * @param Doku_Renderer_plugin_data_edit $renderer * @deprecated _editData() is used since Igor */ protected function editDataLegacy($data, &$renderer) { $renderer->form->startFieldset($this->getLang('dataentry')); $renderer->form->_content[count($renderer->form->_content) - 1]['class'] = 'plugin__data'; $renderer->form->addHidden('range', '0-0'); // Adora Belle bugfix if ($this->getConf('edit_content_only')) { $renderer->form->addHidden('data_edit[classes]', $data['classes']); $columns = ['title', 'value', 'comment']; $class = 'edit_content_only'; } else { $renderer->form->addElement( form_makeField( 'text', 'data_edit[classes]', $data['classes'], $this->getLang('class'), 'data__classes' ) ); $columns = ['title', 'type', 'multi', 'value', 'comment']; $class = 'edit_all_content'; // New line $data['data'][''] = ''; $data['cols'][''] = ['type' => '', 'multi' => false]; } $renderer->form->addElement(""); //header $header = ''; foreach ($columns as $column) { $header .= ''; } $header .= ''; $renderer->form->addElement($header); //rows $n = 0; foreach ($data['cols'] as $key => $vals) { $fieldid = 'data_edit[data][' . $n++ . ']'; $content = $vals['multi'] ? implode(', ', $data['data'][$key]) : $data['data'][$key]; if (is_array($vals['type'])) { $vals['basetype'] = $vals['type']['type']; if (isset($vals['type']['enum'])) { $vals['enum'] = $vals['type']['enum']; } $vals['type'] = $vals['origtype']; } else { $vals['basetype'] = $vals['type']; } if ($vals['type'] === 'hidden') { $renderer->form->addElement(''); } else { $renderer->form->addElement(''); } if ($this->getConf('edit_content_only')) { if (isset($vals['enum'])) { $values = preg_split('/\s*,\s*/', $vals['enum']); if (!$vals['multi']) { array_unshift($values, ''); } $content = form_makeListboxField( $fieldid . '[value][]', $values, $data['data'][$key], $vals['title'], '', '', ($vals['multi'] ? ['multiple' => 'multiple'] : []) ); } else { $classes = 'data_type_' . $vals['type'] . ($vals['multi'] ? 's' : '') . ' ' . 'data_type_' . $vals['basetype'] . ($vals['multi'] ? 's' : ''); $attr = []; if ($vals['basetype'] == 'date' && !$vals['multi']) { $attr['class'] = 'datepicker'; } $content = form_makeField( 'text', $fieldid . '[value]', $content, $vals['title'], '', $classes, $attr ); } $cells = [ hsc($vals['title']) . ':', $content, '' . hsc($vals['comment']) . '' ]; foreach (['multi', 'comment', 'type'] as $field) { $renderer->form->addHidden($fieldid . "[$field]", $vals[$field]); } //keep key as key, even if title is translated $renderer->form->addHidden($fieldid . "[title]", $vals['origkey']); } else { $check_data = $vals['multi'] ? ['checked' => 'checked'] : []; $cells = [ form_makeField('text', $fieldid . '[title]', $vals['origkey'], $this->getLang('title')), // when editable, always use the pure key, not a title form_makeMenuField( $fieldid . '[type]', array_merge( ['', 'page', 'nspage', 'title', 'img', 'mail', 'url', 'tag', 'wiki', 'dt', 'hidden'], array_keys($this->dthlp->aliases()) ), $vals['type'], $this->getLang('type') ), form_makeCheckboxField( $fieldid . '[multi]', ['1', ''], $this->getLang('multi'), '', '', $check_data ), form_makeField( 'text', $fieldid . '[value]', $content, $this->getLang('value') ), form_makeField( 'text', $fieldid . '[comment]', $vals['comment'], $this->getLang('comment'), '', 'data_comment', ['readonly' => 1, 'title' => $vals['comment']] ), ]; } foreach ($cells as $index => $cell) { $renderer->form->addElement("'); } $renderer->form->addElement(''); } $renderer->form->addElement('
' . $this->getLang($column) . '
"); $renderer->form->addElement($cell); $renderer->form->addElement('
'); $renderer->form->endFieldset(); } /** * The custom editor for editing data entries * * Gets called from action_plugin_data::_editform() where also the form member is attached * * @param array $data * @param Doku_Renderer_plugin_data_edit $renderer */ protected function editData($data, &$renderer) { $renderer->form->addFieldsetOpen($this->getLang('dataentry'))->attr('class', 'plugin__data'); if ($this->getConf('edit_content_only')) { $renderer->form->setHiddenField('data_edit[classes]', $data['classes']); $columns = ['title', 'value', 'comment']; $class = 'edit_content_only'; } else { $renderer->form->addTextInput('data_edit[classes]', $this->getLang('class')) ->id('data__classes') ->val($data['classes']); $columns = ['title', 'type', 'multi', 'value', 'comment']; $class = 'edit_all_content'; // New line $data['data'][''] = ''; $data['cols'][''] = ['type' => '', 'multi' => false]; } $renderer->form->addHTML(""); //header $header = ''; foreach ($columns as $column) { $header .= ''; } $header .= ''; $renderer->form->addHTML($header); //rows $n = 0; foreach ($data['cols'] as $key => $vals) { $fieldid = 'data_edit[data][' . $n++ . ']'; $content = $vals['multi'] ? implode(', ', $data['data'][$key]) : $data['data'][$key]; if (is_array($vals['type'])) { $vals['basetype'] = $vals['type']['type']; if (isset($vals['type']['enum'])) { $vals['enum'] = $vals['type']['enum']; } $vals['type'] = $vals['origtype']; } else { $vals['basetype'] = $vals['type']; } if ($vals['type'] === 'hidden') { $renderer->form->addHTML(''); } else { $renderer->form->addHTML(''); } if ($this->getConf('edit_content_only')) { if (isset($vals['enum'])) { $values = preg_split('/\s*,\s*/', $vals['enum']); if (!$vals['multi']) { array_unshift($values, ''); } $el = new DropdownElement( $fieldid . '[value]', $values, $vals['title'] ); $el->useInput(false); $el->attrs(($vals['multi'] ? ['multiple' => 'multiple'] : [])); $el->attr('selected', $data['data'][$key]); $el->val($data['data'][$key]); } else { $classes = 'data_type_' . $vals['type'] . ($vals['multi'] ? 's' : '') . ' ' . 'data_type_' . $vals['basetype'] . ($vals['multi'] ? 's' : ''); $attr = []; if ($vals['basetype'] == 'date' && !$vals['multi']) { $attr['class'] = 'datepicker'; } $el = new InputElement('text', $fieldid . '[value]', $vals['title']); $el->useInput(false); $el->val($content); $el->addClass($classes); $el->attrs($attr); } $cells = [ hsc($vals['title']) . ':', $el, '' . hsc($vals['comment'] ?? '') . '' ]; foreach (['multi', 'comment', 'type'] as $field) { $renderer->form->setHiddenField($fieldid . "[$field]", $vals[$field] ?? ''); } //keep key as key, even if title is translated $renderer->form->setHiddenField($fieldid . "[title]", $vals['origkey'] ?? ''); } else { $check_data = $vals['multi'] ? ['checked' => 'checked'] : []; $cells = []; $el = new InputElement('text', $fieldid . '[title]', $this->getLang('title')); $el->val($vals['origkey'] ?? ''); $cells[] = $el; $el = new \dokuwiki\Form\DropdownElement( $fieldid . '[type]', array_merge( ['', 'page', 'nspage', 'title', 'img', 'mail', 'url', 'tag', 'wiki', 'dt', 'hidden'], array_keys($this->dthlp->aliases()) ), $this->getLang('type') ); $el->val($vals['type']); $cells[] = $el; $el = new CheckableElement('checkbox', $fieldid . '[multi]', $this->getLang('multi')); $el->attrs($check_data); $cells[] = $el; $el = new InputElement('text', $fieldid . '[value]', $this->getLang('value')); $el->val($content); $cells[] = $el; $el = new InputElement('text', $fieldid . '[comment]', $this->getLang('comment')); $el->addClass('data_comment'); $el->attrs(['readonly' => '1', 'title' => $vals['comment'] ?? '']); $el->val($vals['comment'] ?? ''); $cells[] = $el; } foreach ($cells as $index => $cell) { $renderer->form->addHTML("'); } $renderer->form->addHTML(''); } $renderer->form->addHTML('
' . $this->getLang($column) . '
"); if (is_a($cell, Element::class)) { $renderer->form->addElement($cell); } else { $renderer->form->addHTML($cell); } $renderer->form->addHTML('
'); $renderer->form->addFieldsetClose(); } /** * Escapes the given value against being handled as comment * * @param $txt * @return mixed * @todo bad naming */ public static function normalize($txt) { return str_replace('#', '\#', trim($txt)); } /** * Handles the data posted from the editor to recreate the entry syntax * * @param array $data data given via POST * @return string */ public static function editToWiki($data) { $nudata = []; $len = 0; // we check the maximum lenght for nice alignment later foreach ($data['data'] as $field) { if (is_array($field['value'])) { $field['value'] = implode(', ', $field['value']); } $field = array_map('trim', $field); if ($field['title'] === '') continue; $name = syntax_plugin_data_entry::normalize($field['title']); if ($field['type'] !== '') { $name .= '_' . syntax_plugin_data_entry::normalize($field['type']); } elseif (substr($name, -1, 1) === 's') { $name .= '_'; // when the field name ends in 's' we need to secure it against being assumed as multi } // 's' is added to either type or name for multi if ($field['multi'] === '1') { $name .= 's'; } $nudata[] = [$name, syntax_plugin_data_entry::normalize($field['value']), $field['comment']]; $len = max($len, PhpString::strlen($nudata[count($nudata) - 1][0])); } $ret = '---- dataentry ' . trim($data['classes']) . ' ----' . DOKU_LF; foreach ($nudata as $field) { $ret .= $field[0] . str_repeat(' ', $len + 1 - PhpString::strlen($field[0])) . ': '; $ret .= $field[1]; if ($field[2] !== '') { $ret .= ' # ' . $field[2]; } $ret .= DOKU_LF; } $ret .= "----\n"; return $ret; } }