1<?php
2/**
3 *
4 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
5 * @author     Andreas Gohr <andi@splitbrain.org>
6 */
7// must be run within Dokuwiki
8if(!defined('DOKU_INC')) die();
9
10/**
11 * Class syntax_plugin_dataau_entry
12 */
13class syntax_plugin_dataau_entry extends DokuWiki_Syntax_Plugin {
14
15    /**
16     * @var helper_plugin_dataau will hold the dataau helper plugin
17     */
18    var $dthlp = null;
19
20    /**
21     * Constructor. Load helper plugin
22     */
23    function __construct() {
24        $this->dthlp = plugin_load('helper', 'dataau');
25        if(!$this->dthlp) msg('Loading the dataau helper failed. Make sure the dataau 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_dataau_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        $dataau = 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($dataau[$column['key']])) {
93                    // init with empty array
94                    // Note that multiple occurrences of the field are
95                    // practically merged
96                    $dataau[$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, $dataau[$column['key']])) {
103                        $dataau[$column['key']][] = $val;
104                    }
105                }
106            } else {
107                $dataau[$column['key']] = $this->dthlp->_cleanData($line[1], $column['type']);
108            }
109            $columns[$column['key']] = $column;
110        }
111        return array(
112            'dataau' => $dataau, '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   $dataau     array         data created by handler()
123     * @return  boolean                 rendered correctly?
124     */
125    function render($format, Doku_Renderer $renderer, $dataau) {
126        if(is_null($dataau)) 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($dataau, $renderer);
134                return true;
135            case 'metadata':
136                /** @var $renderer Doku_Renderer_metadata */
137                $this->_saveData($dataau, $ID, $renderer->meta['title']);
138                return true;
139            case 'plugin_dataau_edit':
140                /** @var $renderer Doku_Renderer_plugin_dataau_edit */
141                $this->_editData($dataau, $renderer);
142                return true;
143            default:
144                return false;
145        }
146    }
147
148    /**
149     * Output the data in a table
150     *
151     * @param array               $data
152     * @param Doku_Renderer_xhtml $R
153     */
154    function _showData($dataau, $R) {
155        global $ID;
156        $ret = '';
157
158        $sectionEditData = ['target' => 'plugin_dataau'];
159        if (!defined('SEC_EDIT_PATTERN')) {
160            // backwards-compatibility for Frusterick Manners (2017-02-19)
161            $sectionEditData = 'plugin_dataau';
162        }
163        $dataau['classes'] .= ' ' . $R->startSectionEdit($dataau['pos'], $sectionEditData);
164
165        $ret .= '<div class="inline dataauplugin_entry ' . $dataau['classes'] . '"><dl>';
166        $class_names = array();
167        foreach($dataau['dataau'] as $key => $val) {
168            if($val == '' || !count($val)) continue;
169            $type = $dataau['cols'][$key]['type'];
170            if(is_array($type)) {
171                $type = $type['type'];
172            }
173            if($type === 'hidden') continue;
174
175            $class_name = hsc(sectionID($key, $class_names));
176            $ret .= '<dt class="' . $class_name . '">' . hsc($dataau['cols'][$key]['title']) . '<span class="sep">: </span></dt>';
177            $ret .= '<dd class="' . $class_name . '">';
178            if(is_array($val)) {
179                $cnt = count($val);
180                for($i = 0; $i < $cnt; $i++) {
181                    switch($type) {
182                        case 'wiki':
183                            $val[$i] = $ID . '|' . $val[$i];
184                            break;
185                    }
186                    $ret .= $this->dthlp->_formatData($dataau['cols'][$key], $val[$i], $R);
187                    if($i < $cnt - 1) {
188                        $ret .= '<span class="sep">, </span>';
189                    }
190                }
191            } else {
192                switch($type) {
193                    case 'wiki':
194                        $val = $ID . '|' . $val;
195                        break;
196                }
197                $ret .= $this->dthlp->_formatData($dataau['cols'][$key], $val, $R);
198            }
199            $ret .= '</dd>';
200        }
201        $ret .= '</dl></div>';
202        $R->doc .= $ret;
203        $R->finishSectionEdit($dataau['len'] + $dataau['pos']);
204    }
205
206    /**
207     * Save date to the database
208     */
209    function _saveData($dataau, $id, $title) {
210        $sqlite = $this->dthlp->_getDB();
211        if(!$sqlite) return false;
212
213        if(!$title) {
214            $title = $id;
215        }
216
217        $class = $dataau['classes'];
218
219        // begin transaction
220        $sqlite->query("BEGIN TRANSACTION");
221
222        // store page info
223        $this->replaceQuery(
224            "INSERT OR IGNORE INTO pages (page,title,class) VALUES (?,?,?)",
225            $id, $title, $class
226        );
227
228        // Update title if insert failed (record already saved before)
229        $revision = filemtime(wikiFN($id));
230        $this->replaceQuery(
231            "UPDATE pages SET title = ?, class = ?, lastmod = ? WHERE page = ?",
232            $title, $class, $revision, $id
233        );
234
235        // fetch page id
236        $res = $this->replaceQuery("SELECT pid FROM pages WHERE page = ?", $id);
237        $pid = (int) $sqlite->res2single($res);
238        $sqlite->res_close($res);
239
240        if(!$pid) {
241            msg("dataau plugin: failed saving data", -1);
242            $sqlite->query("ROLLBACK TRANSACTION");
243            return false;
244        }
245
246        // remove old data
247        $sqlite->query("DELETE FROM DATA WHERE pid = ?", $pid);
248
249        // insert new data
250        foreach($dataau['dataau'] as $key => $val) {
251            if(is_array($val)) foreach($val as $v) {
252                $this->replaceQuery(
253                    "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)",
254                    $pid, $key, $v
255                );
256            } else {
257                $this->replaceQuery(
258                    "INSERT INTO DATA (pid, KEY, VALUE) VALUES (?, ?, ?)",
259                    $pid, $key, $val
260                );
261            }
262        }
263
264        // finish transaction
265        $sqlite->query("COMMIT TRANSACTION");
266
267        return true;
268    }
269
270    /**
271     * @return bool|mixed
272     */
273    function replaceQuery() {
274        $args = func_get_args();
275        $argc = func_num_args();
276
277        if($argc > 1) {
278            for($i = 1; $i < $argc; $i++) {
279                $dataau = array();
280                $dataau['sql'] = $args[$i];
281                $this->dthlp->_replacePlaceholdersInSQL($dataau);
282                $args[$i] = $dataau['sql'];
283            }
284        }
285
286        $sqlite = $this->dthlp->_getDB();
287        if(!$sqlite) return false;
288
289        return call_user_func_array(array(&$sqlite, 'query'), $args);
290    }
291
292    /**
293     * The custom editor for editing data entries
294     *
295     * Gets called from action_plugin_dataau::_editform() where also the form member is attached
296     *
297     * @param array                          $data
298     * @param Doku_Renderer_plugin_dataau_edit $renderer
299     */
300    function _editData($dataau, &$renderer) {
301        $renderer->form->startFieldset($this->getLang('dataentry'));
302        $renderer->form->_content[count($renderer->form->_content) - 1]['class'] = 'plugin__dataau';
303        $renderer->form->addHidden('range', '0-0'); // Adora Belle bugfix
304
305        if($this->getConf('edit_content_only')) {
306            $renderer->form->addHidden('dataau_edit[classes]', $dataau['classes']);
307
308            $columns = array('title', 'value', 'comment');
309            $class = 'edit_content_only';
310
311        } else {
312            $renderer->form->addElement(form_makeField('text', 'dataau_edit[classes]', $dataau['classes'], $this->getLang('class'), 'dataau__classes'));
313
314            $columns = array('title', 'type', 'multi', 'value', 'comment');
315            $class = 'edit_all_content';
316
317            // New line
318            $dataau['dataau'][''] = '';
319            $dataau['cols'][''] = array('type' => '', 'multi' => false);
320        }
321
322        $renderer->form->addElement("<table class=\"$class\">");
323
324        //header
325        $header = '<tr>';
326        foreach($columns as $column) {
327            $header .= '<th class="' . $column . '">' . $this->getLang($column) . '</th>';
328        }
329        $header .= '</tr>';
330        $renderer->form->addElement($header);
331
332        //rows
333        $n = 0;
334        foreach($dataau['cols'] as $key => $vals) {
335            $fieldid = 'dataau_edit[dataau][' . $n++ . ']';
336            $content = $vals['multi'] ? implode(', ', $dataau['dataau'][$key]) : $dataau['dataau'][$key];
337            if(is_array($vals['type'])) {
338                $vals['basetype'] = $vals['type']['type'];
339                if(isset($vals['type']['enum'])) {
340                    $vals['enum'] = $vals['type']['enum'];
341                }
342                $vals['type'] = $vals['origtype'];
343            } else {
344                $vals['basetype'] = $vals['type'];
345            }
346
347            if($vals['type'] === 'hidden') {
348                $renderer->form->addElement('<tr class="hidden">');
349            } else {
350                $renderer->form->addElement('<tr>');
351            }
352            if($this->getConf('edit_content_only')) {
353                if(isset($vals['enum'])) {
354                    $values = preg_split('/\s*,\s*/', $vals['enum']);
355                    if(!$vals['multi']) {
356                        array_unshift($values, '');
357                    }
358                    $content = form_makeListboxField(
359                        $fieldid . '[value][]',
360                        $values,
361                        $dataau['dataau'][$key],
362                        $vals['title'],
363                        '', '',
364                        ($vals['multi'] ? array('multiple' => 'multiple') : array())
365                    );
366                } else {
367                    $classes = 'dataau_type_' . $vals['type'] . ($vals['multi'] ? 's' : '') . ' '
368                        . 'dataau_type_' . $vals['basetype'] . ($vals['multi'] ? 's' : '');
369
370                    $attr = array();
371                    if($vals['basetype'] == 'date' && !$vals['multi']) {
372                        $attr['class'] = 'datepicker';
373                    }
374
375                    $content = form_makeField('text', $fieldid . '[value]', $content, $vals['title'], '', $classes, $attr);
376
377                }
378                $cells = array(
379                    hsc($vals['title']) . ':',
380                    $content,
381                    '<span title="' . hsc($vals['comment']) . '">' . hsc($vals['comment']) . '</span>'
382                );
383                foreach(array('multi', 'comment', 'type') as $field) {
384                    $renderer->form->addHidden($fieldid . "[$field]", $vals[$field]);
385                }
386                $renderer->form->addHidden($fieldid . "[title]", $vals['origkey']); //keep key as key, even if title is translated
387            } else {
388                $check_dataau = $vals['multi'] ? array('checked' => 'checked') : array();
389                $cells = array(
390                    form_makeField('text', $fieldid . '[title]', $vals['origkey'], $this->getLang('title')), // when editable, always use the pure key, not a title
391                    form_makeMenuField(
392                        $fieldid . '[type]',
393                        array_merge(
394                            array(
395                                '', 'page', 'nspage', 'title',
396                                'img', 'mail', 'url', 'tag', 'wiki', 'dt', 'hidden'
397                            ),
398                            array_keys($this->dthlp->_aliases())
399                        ),
400                        $vals['type'],
401                        $this->getLang('type')
402                    ),
403                    form_makeCheckboxField($fieldid . '[multi]', array('1', ''), $this->getLang('multi'), '', '', $check_dataau),
404                    form_makeField('text', $fieldid . '[value]', $content, $this->getLang('value')),
405                    form_makeField('text', $fieldid . '[comment]', $vals['comment'], $this->getLang('comment'), '', 'dataau_comment', array('readonly' => 1, 'title' => $vals['comment']))
406                );
407            }
408
409            foreach($cells as $index => $cell) {
410                $renderer->form->addElement("<td class=\"{$columns[$index]}\">");
411                $renderer->form->addElement($cell);
412                $renderer->form->addElement('</td>');
413            }
414            $renderer->form->addElement('</tr>');
415        }
416
417        $renderer->form->addElement('</table>');
418        $renderer->form->endFieldset();
419    }
420
421    /**
422     * Escapes the given value against being handled as comment
423     *
424     * @todo bad naming
425     * @param $txt
426     * @return mixed
427     */
428    public static function _normalize($txt) {
429        return str_replace('#', '\#', trim($txt));
430    }
431
432    /**
433     * Handles the data posted from the editor to recreate the entry syntax
434     *
435     * @param array $dataau data given via POST
436     * @return string
437     */
438    public static function editToWiki($dataau) {
439        $nudataau = array();
440
441        $len = 0; // we check the maximum lenght for nice alignment later
442        foreach($dataau['dataau'] as $field) {
443            if(is_array($field['value'])) {
444                $field['value'] = join(', ', $field['value']);
445            }
446            $field = array_map('trim', $field);
447            if($field['title'] === '') continue;
448
449            $name = syntax_plugin_dataau_entry::_normalize($field['title']);
450
451            if($field['type'] !== '') {
452                $name .= '_' . syntax_plugin_dataau_entry::_normalize($field['type']);
453            } elseif(substr($name, -1, 1) === 's') {
454                $name .= '_'; // when the field name ends in 's' we need to secure it against being assumed as multi
455            }
456            // 's' is added to either type or name for multi
457            if($field['multi'] === '1') {
458                $name .= 's';
459            }
460
461            $nudataau[] = array($name, syntax_plugin_dataau_entry::_normalize($field['value']), $field['comment']);
462            $len = max($len, utf8_strlen($nudataau[count($nudataau) - 1][0]));
463        }
464
465        $ret = '---- dataentry ' . trim($dataau['classes']) . ' ----' . DOKU_LF;
466        foreach($nudataau as $field) {
467            $ret .= $field[0] . str_repeat(' ', $len + 1 - utf8_strlen($field[0])) . ': ';
468            $ret .= $field[1];
469            if($field[2] !== '') {
470                $ret .= ' # ' . $field[2];
471            }
472            $ret .= DOKU_LF;
473        }
474        $ret .= "----\n";
475        return $ret;
476    }
477}
478