1<?php 2 3/** 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9use dokuwiki\Extension\SyntaxPlugin; 10use dokuwiki\plugin\data\Form\DropdownElement; 11use dokuwiki\Form\InputElement; 12use dokuwiki\Form\CheckableElement; 13use dokuwiki\Form\Element; 14use dokuwiki\Utf8\PhpString; 15 16/** 17 * Class syntax_plugin_data_entry 18 */ 19class syntax_plugin_data_entry extends SyntaxPlugin 20{ 21 /** 22 * @var helper_plugin_data will hold the data helper plugin 23 */ 24 public $dthlp; 25 26 /** 27 * Constructor. Load helper plugin 28 */ 29 public function __construct() 30 { 31 $this->dthlp = plugin_load('helper', 'data'); 32 if (!$this->dthlp) msg('Loading the data helper failed. Make sure the data plugin is installed.', -1); 33 } 34 35 /** 36 * What kind of syntax are we? 37 */ 38 public function getType() 39 { 40 return 'substition'; 41 } 42 43 /** 44 * What about paragraphs? 45 */ 46 public function getPType() 47 { 48 return 'block'; 49 } 50 51 /** 52 * Where to sort in? 53 */ 54 public function getSort() 55 { 56 return 155; 57 } 58 59 /** 60 * Connect pattern to lexer 61 */ 62 public function connectTo($mode) 63 { 64 $this->Lexer->addSpecialPattern( 65 '----+ *dataentry(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+', 66 $mode, 67 'plugin_data_entry' 68 ); 69 } 70 71 /** 72 * Handle the match - parse the data 73 * 74 * @param string $match The text matched by the patterns 75 * @param int $state The lexer state for the match 76 * @param int $pos The character position of the matched text 77 * @param Doku_Handler $handler The Doku_Handler object 78 * @return bool|array Return an array with all data you want to use in render, false don't add an instruction 79 */ 80 public function handle($match, $state, $pos, Doku_Handler $handler) 81 { 82 if (!$this->dthlp->ready()) return null; 83 84 // get lines 85 $lines = explode("\n", $match); 86 array_pop($lines); 87 $class = array_shift($lines); 88 $class = str_replace('dataentry', '', $class); 89 $class = trim($class, '- '); 90 91 // parse info 92 $data = []; 93 $columns = []; 94 foreach ($lines as $line) { 95 // ignore comments 96 preg_match('/^(.*?(?<![&\\\\]))(?:#(.*))?$/', $line, $matches); 97 $line = $matches[1]; 98 $line = str_replace('\\#', '#', $line); 99 $line = trim($line); 100 if (empty($line)) continue; 101 $line = preg_split('/\s*:\s*/', $line, 2); 102 103 $column = $this->dthlp->column($line[0]); 104 if (isset($matches[2])) { 105 $column['comment'] = $matches[2]; 106 } 107 if ($column['multi']) { 108 if (!isset($data[$column['key']])) { 109 // init with empty array 110 // Note that multiple occurrences of the field are 111 // practically merged 112 $data[$column['key']] = []; 113 } 114 $vals = explode(',', $line[1]); 115 foreach ($vals as $val) { 116 $val = trim($this->dthlp->cleanData($val, $column['type'])); 117 if ($val == '') continue; 118 if (!in_array($val, $data[$column['key']])) { 119 $data[$column['key']][] = $val; 120 } 121 } 122 } else { 123 $data[$column['key']] = $this->dthlp->cleanData($line[1] ?? '', $column['type']); 124 } 125 $columns[$column['key']] = $column; 126 } 127 return [ 128 'data' => $data, 129 'cols' => $columns, 130 'classes' => $class, 131 'pos' => $pos, 132 'len' => strlen($match) // not utf8_strlen 133 ]; 134 } 135 136 /** 137 * Create output or save the data 138 * 139 * @param $format string output format being rendered 140 * @param $renderer Doku_Renderer the current renderer object 141 * @param $data array data created by handler() 142 * @return boolean rendered correctly? 143 */ 144 public function render($format, Doku_Renderer $renderer, $data) 145 { 146 if (is_null($data)) return false; 147 if (!$this->dthlp->ready()) return false; 148 149 global $ID; 150 switch ($format) { 151 case 'xhtml': 152 /** @var $renderer Doku_Renderer_xhtml */ 153 $this->showData($data, $renderer); 154 return true; 155 case 'metadata': 156 /** @var $renderer Doku_Renderer_metadata */ 157 $this->saveData($data, $ID, $renderer->meta['title'] ?? ''); 158 return true; 159 case 'plugin_data_edit': 160 /** @var $renderer Doku_Renderer_plugin_data_edit */ 161 if (is_a($renderer->form, 'Doku_Form')) { 162 $this->editDataLegacy($data, $renderer); 163 } else { 164 $this->editData($data, $renderer); 165 } 166 return true; 167 default: 168 return false; 169 } 170 } 171 172 /** 173 * Output the data in a table 174 * 175 * @param array $data 176 * @param Doku_Renderer_xhtml $R 177 */ 178 public function showData($data, $R) 179 { 180 global $ID; 181 $ret = ''; 182 183 $sectionEditData = ['target' => 'plugin_data', 'hid' => 'data_entry']; 184 $data['classes'] .= ' ' . $R->startSectionEdit($data['pos'], $sectionEditData); 185 186 $ret .= '<div class="inline dataplugin_entry ' . $data['classes'] . '"><dl>'; 187 $class_names = []; 188 foreach ($data['data'] as $key => $val) { 189 if ($val == '' || is_null($val) || (is_array($val) && count($val) == 0)) continue; 190 $type = $data['cols'][$key]['type']; 191 if (is_array($type)) { 192 $type = $type['type']; 193 } 194 if ($type === 'hidden') continue; 195 196 $class_name = hsc(sectionID($key, $class_names)); 197 $ret .= '<dt class="' . $class_name . '">' . 198 hsc($data['cols'][$key]['title']) . 199 '<span class="sep">: </span></dt>'; 200 $ret .= '<dd class="' . $class_name . '">'; 201 if (is_array($val)) { 202 $cnt = count($val); 203 for ($i = 0; $i < $cnt; $i++) { 204 if ($type === 'wiki') { 205 $val[$i] = $ID . '|' . $val[$i]; 206 } 207 $ret .= $this->dthlp->formatData($data['cols'][$key], $val[$i], $R); 208 if ($i < $cnt - 1) { 209 $ret .= '<span class="sep">, </span>'; 210 } 211 } 212 } else { 213 if ($type === 'wiki') { 214 $val = $ID . '|' . $val; 215 } 216 $ret .= $this->dthlp->formatData($data['cols'][$key], $val, $R); 217 } 218 $ret .= '</dd>'; 219 } 220 $ret .= '</dl></div>'; 221 $R->doc .= $ret; 222 $R->finishSectionEdit($data['len'] + $data['pos']); 223 } 224 225 /** 226 * Save date to the database 227 */ 228 public function saveData($data, $id, $title) 229 { 230 $sqlite = $this->dthlp->getDB(); 231 if (!$sqlite) return false; 232 233 if (!$title) { 234 $title = $id; 235 } 236 237 $class = $data['classes']; 238 239 $sqlite->getPdo()->beginTransaction(); 240 try { 241 // store page info 242 $this->replaceQuery( 243 "INSERT OR IGNORE INTO pages (page,title,class) VALUES (?,?,?)", 244 $id, 245 $title, 246 $class 247 ); 248 249 // Update title if insert failed (record already saved before) 250 $revision = filemtime(wikiFN($id)); 251 $this->replaceQuery( 252 "UPDATE pages SET title = ?, class = ?, lastmod = ? WHERE page = ?", 253 $title, 254 $class, 255 $revision, 256 $id 257 ); 258 259 // fetch page id 260 /** @var PDOStatement $res */ 261 $res = $this->replaceQuery("SELECT pid FROM pages WHERE page = ?", $id); 262 $all = $res->fetchAll(\PDO::FETCH_ASSOC); 263 $res->closeCursor(); 264 $pid = (int)$all[0]['pid']; 265 if (!$pid) { 266 throw new Exception("data plugin: failed saving data"); 267 } 268 269 // remove old data 270 $sqlite->query("DELETE FROM DATA WHERE pid = ?", $pid); 271 272 // insert new data 273 foreach ($data['data'] as $key => $val) { 274 if (is_array($val)) foreach ($val as $v) { 275 $this->replaceQuery( 276 "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)", 277 $pid, 278 $key, 279 $v 280 ); 281 } else { 282 $this->replaceQuery( 283 "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)", 284 $pid, 285 $key, 286 $val 287 ); 288 } 289 } 290 291 // finish transaction 292 $sqlite->getPdo()->commit(); 293 } catch (\Exception $exception) { 294 $sqlite->getPdo()->rollBack(); 295 msg(hsc($exception->getMessage()), -1); 296 } 297 298 return true; 299 } 300 301 /** 302 * 303 * @fixme replace this madness 304 * @return bool|mixed 305 */ 306 public function replaceQuery() 307 { 308 $args = func_get_args(); 309 $argc = func_num_args(); 310 311 if ($argc > 1) { 312 for ($i = 1; $i < $argc; $i++) { 313 $data = []; 314 $data['sql'] = $args[$i]; 315 $this->dthlp->replacePlaceholdersInSQL($data); 316 $args[$i] = $data['sql']; 317 } 318 } 319 320 $sqlite = $this->dthlp->getDB(); 321 if (!$sqlite) return false; 322 323 return call_user_func_array(array(&$sqlite, 'query'), $args); 324 } 325 326 327 /** 328 * The custom editor for editing data entries 329 * 330 * Gets called from action_plugin_data::_editform() where also the form member is attached 331 * 332 * @param array $data 333 * @param Doku_Renderer_plugin_data_edit $renderer 334 * @deprecated _editData() is used since Igor 335 */ 336 protected function editDataLegacy($data, &$renderer) 337 { 338 $renderer->form->startFieldset($this->getLang('dataentry')); 339 $renderer->form->_content[count($renderer->form->_content) - 1]['class'] = 'plugin__data'; 340 $renderer->form->addHidden('range', '0-0'); // Adora Belle bugfix 341 342 if ($this->getConf('edit_content_only')) { 343 $renderer->form->addHidden('data_edit[classes]', $data['classes']); 344 345 $columns = ['title', 'value', 'comment']; 346 $class = 'edit_content_only'; 347 } else { 348 $renderer->form->addElement( 349 form_makeField( 350 'text', 351 'data_edit[classes]', 352 $data['classes'], 353 $this->getLang('class'), 354 'data__classes' 355 ) 356 ); 357 358 $columns = ['title', 'type', 'multi', 'value', 'comment']; 359 $class = 'edit_all_content'; 360 361 // New line 362 $data['data'][''] = ''; 363 $data['cols'][''] = ['type' => '', 'multi' => false]; 364 } 365 366 $renderer->form->addElement("<table class=\"$class\">"); 367 368 //header 369 $header = '<tr>'; 370 foreach ($columns as $column) { 371 $header .= '<th class="' . $column . '">' . $this->getLang($column) . '</th>'; 372 } 373 $header .= '</tr>'; 374 $renderer->form->addElement($header); 375 376 //rows 377 $n = 0; 378 foreach ($data['cols'] as $key => $vals) { 379 $fieldid = 'data_edit[data][' . $n++ . ']'; 380 $content = $vals['multi'] ? implode(', ', $data['data'][$key]) : $data['data'][$key]; 381 if (is_array($vals['type'])) { 382 $vals['basetype'] = $vals['type']['type']; 383 if (isset($vals['type']['enum'])) { 384 $vals['enum'] = $vals['type']['enum']; 385 } 386 $vals['type'] = $vals['origtype']; 387 } else { 388 $vals['basetype'] = $vals['type']; 389 } 390 391 if ($vals['type'] === 'hidden') { 392 $renderer->form->addElement('<tr class="hidden">'); 393 } else { 394 $renderer->form->addElement('<tr>'); 395 } 396 if ($this->getConf('edit_content_only')) { 397 if (isset($vals['enum'])) { 398 $values = preg_split('/\s*,\s*/', $vals['enum']); 399 if (!$vals['multi']) { 400 array_unshift($values, ''); 401 } 402 $content = form_makeListboxField( 403 $fieldid . '[value][]', 404 $values, 405 $data['data'][$key], 406 $vals['title'], 407 '', 408 '', 409 ($vals['multi'] ? ['multiple' => 'multiple'] : []) 410 ); 411 } else { 412 $classes = 'data_type_' . $vals['type'] . ($vals['multi'] ? 's' : '') . ' ' 413 . 'data_type_' . $vals['basetype'] . ($vals['multi'] ? 's' : ''); 414 415 $attr = []; 416 if ($vals['basetype'] == 'date' && !$vals['multi']) { 417 $attr['class'] = 'datepicker'; 418 } 419 420 $content = form_makeField( 421 'text', 422 $fieldid . '[value]', 423 $content, 424 $vals['title'], 425 '', 426 $classes, 427 $attr 428 ); 429 } 430 $cells = [ 431 hsc($vals['title']) . ':', 432 $content, 433 '<span title="' . hsc($vals['comment']) . '">' . hsc($vals['comment']) . '</span>' 434 ]; 435 foreach (['multi', 'comment', 'type'] as $field) { 436 $renderer->form->addHidden($fieldid . "[$field]", $vals[$field]); 437 } 438 //keep key as key, even if title is translated 439 $renderer->form->addHidden($fieldid . "[title]", $vals['origkey']); 440 } else { 441 $check_data = $vals['multi'] ? ['checked' => 'checked'] : []; 442 $cells = [ 443 form_makeField('text', $fieldid . '[title]', $vals['origkey'], $this->getLang('title')), 444 // when editable, always use the pure key, not a title 445 form_makeMenuField( 446 $fieldid . '[type]', 447 array_merge( 448 ['', 'page', 'nspage', 'title', 'img', 'mail', 'url', 'tag', 'wiki', 'dt', 'hidden'], 449 array_keys($this->dthlp->aliases()) 450 ), 451 $vals['type'], 452 $this->getLang('type') 453 ), 454 form_makeCheckboxField( 455 $fieldid . '[multi]', 456 ['1', ''], 457 $this->getLang('multi'), 458 '', 459 '', 460 $check_data 461 ), 462 form_makeField( 463 'text', 464 $fieldid . '[value]', 465 $content, 466 $this->getLang('value') 467 ), 468 form_makeField( 469 'text', 470 $fieldid . '[comment]', 471 $vals['comment'], 472 $this->getLang('comment'), 473 '', 474 'data_comment', 475 ['readonly' => 1, 'title' => $vals['comment']] 476 ), 477 ]; 478 } 479 480 foreach ($cells as $index => $cell) { 481 $renderer->form->addElement("<td class=\"{$columns[$index]}\">"); 482 $renderer->form->addElement($cell); 483 $renderer->form->addElement('</td>'); 484 } 485 $renderer->form->addElement('</tr>'); 486 } 487 488 $renderer->form->addElement('</table>'); 489 $renderer->form->endFieldset(); 490 } 491 492 /** 493 * The custom editor for editing data entries 494 * 495 * Gets called from action_plugin_data::_editform() where also the form member is attached 496 * 497 * @param array $data 498 * @param Doku_Renderer_plugin_data_edit $renderer 499 */ 500 protected function editData($data, &$renderer) 501 { 502 $renderer->form->addFieldsetOpen($this->getLang('dataentry'))->attr('class', 'plugin__data'); 503 504 if ($this->getConf('edit_content_only')) { 505 $renderer->form->setHiddenField('data_edit[classes]', $data['classes']); 506 507 $columns = ['title', 'value', 'comment']; 508 $class = 'edit_content_only'; 509 } else { 510 $renderer->form->addTextInput('data_edit[classes]', $this->getLang('class')) 511 ->id('data__classes') 512 ->val($data['classes']); 513 514 $columns = ['title', 'type', 'multi', 'value', 'comment']; 515 $class = 'edit_all_content'; 516 517 // New line 518 $data['data'][''] = ''; 519 $data['cols'][''] = ['type' => '', 'multi' => false]; 520 } 521 522 $renderer->form->addHTML("<table class=\"$class\">"); 523 524 //header 525 $header = '<tr>'; 526 foreach ($columns as $column) { 527 $header .= '<th class="' . $column . '">' . $this->getLang($column) . '</th>'; 528 } 529 $header .= '</tr>'; 530 $renderer->form->addHTML($header); 531 532 //rows 533 $n = 0; 534 foreach ($data['cols'] as $key => $vals) { 535 $fieldid = 'data_edit[data][' . $n++ . ']'; 536 $content = $vals['multi'] ? implode(', ', $data['data'][$key]) : $data['data'][$key]; 537 if (is_array($vals['type'])) { 538 $vals['basetype'] = $vals['type']['type']; 539 if (isset($vals['type']['enum'])) { 540 $vals['enum'] = $vals['type']['enum']; 541 } 542 $vals['type'] = $vals['origtype']; 543 } else { 544 $vals['basetype'] = $vals['type']; 545 } 546 547 if ($vals['type'] === 'hidden') { 548 $renderer->form->addHTML('<tr class="hidden">'); 549 } else { 550 $renderer->form->addHTML('<tr>'); 551 } 552 if ($this->getConf('edit_content_only')) { 553 if (isset($vals['enum'])) { 554 $values = preg_split('/\s*,\s*/', $vals['enum']); 555 if (!$vals['multi']) { 556 array_unshift($values, ''); 557 } 558 559 $el = new DropdownElement( 560 $fieldid . '[value]', 561 $values, 562 $vals['title'] 563 ); 564 $el->useInput(false); 565 $el->attrs(($vals['multi'] ? ['multiple' => 'multiple'] : [])); 566 $el->attr('selected', $data['data'][$key]); 567 $el->val($data['data'][$key]); 568 } else { 569 $classes = 'data_type_' . $vals['type'] . ($vals['multi'] ? 's' : '') . ' ' 570 . 'data_type_' . $vals['basetype'] . ($vals['multi'] ? 's' : ''); 571 572 $attr = []; 573 if ($vals['basetype'] == 'date' && !$vals['multi']) { 574 $attr['class'] = 'datepicker'; 575 } 576 577 $el = new InputElement('text', $fieldid . '[value]', $vals['title']); 578 $el->useInput(false); 579 $el->val($content); 580 $el->addClass($classes); 581 $el->attrs($attr); 582 } 583 $cells = [ 584 hsc($vals['title']) . ':', $el, 585 '<span title="' . hsc($vals['comment'] ?? '') . '">' . hsc($vals['comment'] ?? '') . '</span>' 586 ]; 587 foreach (['multi', 'comment', 'type'] as $field) { 588 $renderer->form->setHiddenField($fieldid . "[$field]", $vals[$field] ?? ''); 589 } 590 //keep key as key, even if title is translated 591 $renderer->form->setHiddenField($fieldid . "[title]", $vals['origkey'] ?? ''); 592 } else { 593 $check_data = $vals['multi'] ? ['checked' => 'checked'] : []; 594 $cells = []; 595 596 $el = new InputElement('text', $fieldid . '[title]', $this->getLang('title')); 597 $el->val($vals['origkey'] ?? ''); 598 $cells[] = $el; 599 600 $el = new \dokuwiki\Form\DropdownElement( 601 $fieldid . '[type]', 602 array_merge( 603 ['', 'page', 'nspage', 'title', 'img', 'mail', 'url', 'tag', 'wiki', 'dt', 'hidden'], 604 array_keys($this->dthlp->aliases()) 605 ), 606 $this->getLang('type') 607 ); 608 $el->val($vals['type']); 609 $cells[] = $el; 610 611 $el = new CheckableElement('checkbox', $fieldid . '[multi]', $this->getLang('multi')); 612 $el->attrs($check_data); 613 $cells[] = $el; 614 615 $el = new InputElement('text', $fieldid . '[value]', $this->getLang('value')); 616 $el->val($content); 617 $cells[] = $el; 618 619 $el = new InputElement('text', $fieldid . '[comment]', $this->getLang('comment')); 620 $el->addClass('data_comment'); 621 $el->attrs(['readonly' => '1', 'title' => $vals['comment'] ?? '']); 622 $el->val($vals['comment'] ?? ''); 623 $cells[] = $el; 624 } 625 626 foreach ($cells as $index => $cell) { 627 $renderer->form->addHTML("<td class=\"{$columns[$index]}\">"); 628 if (is_a($cell, Element::class)) { 629 $renderer->form->addElement($cell); 630 } else { 631 $renderer->form->addHTML($cell); 632 } 633 $renderer->form->addHTML('</td>'); 634 } 635 $renderer->form->addHTML('</tr>'); 636 } 637 638 $renderer->form->addHTML('</table>'); 639 $renderer->form->addFieldsetClose(); 640 } 641 642 /** 643 * Escapes the given value against being handled as comment 644 * 645 * @param $txt 646 * @return mixed 647 * @todo bad naming 648 */ 649 public static function normalize($txt) 650 { 651 return str_replace('#', '\#', trim($txt)); 652 } 653 654 /** 655 * Handles the data posted from the editor to recreate the entry syntax 656 * 657 * @param array $data data given via POST 658 * @return string 659 */ 660 public static function editToWiki($data) 661 { 662 $nudata = []; 663 664 $len = 0; // we check the maximum lenght for nice alignment later 665 foreach ($data['data'] as $field) { 666 if (is_array($field['value'])) { 667 $field['value'] = implode(', ', $field['value']); 668 } 669 $field = array_map('trim', $field); 670 if ($field['title'] === '') continue; 671 672 $name = syntax_plugin_data_entry::normalize($field['title']); 673 674 if ($field['type'] !== '') { 675 $name .= '_' . syntax_plugin_data_entry::normalize($field['type']); 676 } elseif (substr($name, -1, 1) === 's') { 677 $name .= '_'; // when the field name ends in 's' we need to secure it against being assumed as multi 678 } 679 // 's' is added to either type or name for multi 680 if ($field['multi'] === '1') { 681 $name .= 's'; 682 } 683 684 $nudata[] = [$name, syntax_plugin_data_entry::normalize($field['value']), $field['comment']]; 685 $len = max($len, PhpString::strlen($nudata[count($nudata) - 1][0])); 686 } 687 688 $ret = '---- dataentry ' . trim($data['classes']) . ' ----' . DOKU_LF; 689 foreach ($nudata as $field) { 690 $ret .= $field[0] . str_repeat(' ', $len + 1 - PhpString::strlen($field[0])) . ': '; 691 $ret .= $field[1]; 692 if ($field[2] !== '') { 693 $ret .= ' # ' . $field[2]; 694 } 695 $ret .= DOKU_LF; 696 } 697 $ret .= "----\n"; 698 return $ret; 699 } 700} 701