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