*/
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 .= '' . $this->getLang($column) . ' | ';
}
$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($cell);
$renderer->form->addElement(' | ');
}
$renderer->form->addElement('
');
}
$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 .= '' . $this->getLang($column) . ' | ';
}
$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("");
if (is_a($cell, Element::class)) {
$renderer->form->addElement($cell);
} else {
$renderer->form->addHTML($cell);
}
$renderer->form->addHTML(' | ');
}
$renderer->form->addHTML('
');
}
$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;
}
}