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