1<?php
2/**
3 * Bureaucracy-AU Plugin: Allows flexible creation of forms
4 *
5 * This plugin allows definition of forms in wiki pages. The forms can be
6 * submitted via email or used to create new pages from templates.
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Andreas Gohr <andi@splitbrain.org>
10 * @author     Adrian Lang <dokuwiki@cosmocode.de>
11 */
12// must be run within Dokuwiki
13if(!defined('DOKU_INC')) die();
14
15/**
16 * All DokuWiki plugins to extend the parser/rendering mechanism
17 * need to inherit from this class
18 */
19class syntax_plugin_bureaucracyau extends DokuWiki_Syntax_Plugin {
20
21    private $form_id = 0;
22    var $patterns = array();
23    var $values = array();
24
25    /**
26     * Prepare some replacements
27     */
28    public function __construct() {
29        $this->prepareDateTimereplacements();
30        $this->prepareNamespacetemplateReplacements();
31    }
32
33    /**
34     * What kind of syntax are we?
35     */
36    public function getType() {
37        return 'substition';
38    }
39
40    /**
41     * What about paragraphs?
42     */
43    public function getPType() {
44        return 'block';
45    }
46
47    /**
48     * Where to sort in?
49     */
50    public function getSort() {
51        return 155;
52    }
53
54    /**
55     * Connect pattern to lexer
56     *
57     * @param string $mode
58     */
59    public function connectTo($mode) {
60        $this->Lexer->addSpecialPattern('<form>.*?</form>', $mode, 'plugin_bureaucracyau');
61    }
62
63    /**
64     * Handler to prepare matched data for the rendering process
65     *
66     * @param   string       $match   The text matched by the patterns
67     * @param   int          $state   The lexer state for the match
68     * @param   int          $pos     The character position of the matched text
69     * @param   Doku_Handler $handler The Doku_Handler object
70     * @return  bool|array Return an array with all data you want to use in render, false don't add an instruction
71     */
72    public function handle($match, $state, $pos, Doku_Handler $handler) {
73        $match = substr($match, 6, -7); // remove form wrap
74        $lines = explode("\n", $match);
75        $actions = $rawactions = array();
76        $thanks = '';
77        $labels = '';
78
79        // parse the lines into an command/argument array
80        $cmds = array();
81        while(count($lines) > 0) {
82            $line = trim(array_shift($lines));
83            if(!$line) continue;
84            $args = $this->_parse_line($line, $lines);
85            $args[0] = $this->_sanitizeClassName($args[0]);
86
87            if(in_array($args[0], array('action', 'thanks', 'labels'))) {
88                if(count($args) < 2) {
89                    msg(sprintf($this->getLang('e_missingargs'), hsc($args[0]), hsc($args[1])), -1);
90                    continue;
91                }
92
93                // is action element?
94                if($args[0] == 'action') {
95                    array_shift($args);
96                    $rawactions[] = array('type' => array_shift($args), 'argv' => $args);
97                    continue;
98                }
99
100                // is thank you text?
101                if($args[0] == 'thanks') {
102                    $thanks = $args[1];
103                    continue;
104                }
105
106                // is labels?
107                if($args[0] == 'labels') {
108                    $labels = $args[1];
109                    continue;
110                }
111            }
112
113            if(strpos($args[0], '_') === false) {
114                $name = 'bureaucracyau_field' . $args[0];
115            } else {
116                //name convention: plugin_componentname
117                $name = $args[0];
118            }
119
120            /** @var helper_plugin_bureaucracyau_field $field */
121            $field = $this->loadHelper($name, false);
122            if($field && is_a($field, 'helper_plugin_bureaucracyau_field')) {
123                $field->initialize($args);
124                $cmds[] = $field;
125            } else {
126                $evdata = array('fields' => &$cmds, 'args' => $args);
127                $event = new Doku_Event('PLUGIN_BUREAUCRACYAU_FIELD_UNKNOWN', $evdata);
128                if($event->advise_before()) {
129                    msg(sprintf($this->getLang('e_unknowntype'), hsc($name)), -1);
130                }
131            }
132
133        }
134
135        // check if action is available
136        foreach($rawactions as $action) {
137            $action['type'] = $this->_sanitizeClassName($action['type']);
138
139            if(strpos($action['type'], '_') === false) {
140                $action['actionname'] = 'bureaucracyau_action' . $action['type'];
141            } else {
142                //name convention for other plugins: plugin_componentname
143                $action['actionname'] = $action['type'];
144            }
145
146            list($plugin, $component) = explode('_', $action['actionname']);
147            $alternativename = $action['type'] . '_'. $action['type'];
148
149            // bureaucracyau_action<name> or <plugin>_<componentname>
150            if(!plugin_isdisabled($action['actionname']) || @file_exists(DOKU_PLUGIN . $plugin . '/helper/'  . $component . '.php')) {
151                $actions[] = $action;
152
153            // shortcut for other plugins with component name <name>_<name>
154            } elseif(plugin_isdisabled($alternativename) || !@file_exists(DOKU_PLUGIN . $action['type'] . '/helper/'  . $action['type'] . '.php')) {
155                $action['actionname'] = $alternativename;
156                $actions[] = $action;
157
158            // not found
159            } else {
160                $evdata = array('actions' => &$actions, 'action' => $action);
161                $event = new Doku_Event('PLUGIN_BUREAUCRACYAU_ACTION_UNKNOWN', $evdata);
162                if($event->advise_before()) {
163                    msg(sprintf($this->getLang('e_unknownaction'), hsc($action['actionname'])), -1);
164                }
165            }
166        }
167
168        // action(s) found?
169        if(count($actions) < 1) {
170            msg($this->getLang('e_noaction'), -1);
171        }
172
173        // set thank you message
174        if(!$thanks) {
175            $thanks = "";
176            foreach($actions as $action) {
177                $thanks .= $this->getLang($action['type'] . '_thanks');
178            }
179        } else {
180            $thanks = hsc($thanks);
181        }
182        return array(
183            'fields'  => $cmds,
184            'actions' => $actions,
185            'thanks'  => $thanks,
186            'labels'  => $labels
187        );
188    }
189
190    /**
191     * Handles the actual output creation.
192     *
193     * @param string          $format   output format being rendered
194     * @param Doku_Renderer   $R        the current renderer object
195     * @param array           $data     data created by handler()
196     * @return  boolean                 rendered correctly? (however, returned value is not used at the moment)
197     */
198    public function render($format, Doku_Renderer $R, $data) {
199        if($format != 'xhtml') return false;
200        $R->info['cache'] = false; // don't cache
201
202        /**
203         * replace some time and name placeholders in the default values
204         * @var $field helper_plugin_bureaucracyau_field
205         */
206        foreach($data['fields'] as &$field) {
207            if(isset($field->opt['value'])) {
208                $field->opt['value'] = $this->replace($field->opt['value']);
209            }
210        }
211
212        if($data['labels']) $this->loadlabels($data);
213
214        $this->form_id++;
215        if(isset($_POST['bureaucracyau']) && checkSecurityToken() && $_POST['bureaucracyau']['$$id'] == $this->form_id) {
216            $success = $this->_handlepost($data);
217            if($success !== false) {
218                $R->doc .= '<div class="bureaucracyau__plugin" id="scroll__here">' . $success . '</div>';
219                return true;
220            }
221        }
222
223        $R->doc .= $this->_htmlform($data['fields']);
224
225        return true;
226    }
227
228    /**
229     * Initializes the labels, loaded from a defined labelpage
230     *
231     * @param array $data all data passed to render()
232     */
233    protected function loadlabels(&$data) {
234        global $INFO;
235        $labelpage = $data['labels'];
236        $exists = false;
237        resolve_pageid($INFO['namespace'], $labelpage, $exists);
238        if(!$exists) {
239            msg(sprintf($this->getLang('e_labelpage'), html_wikilink($labelpage)), -1);
240            return;
241        }
242
243        // parse simple list (first level cdata only)
244        $labels = array();
245        $instructions = p_cached_instructions(wikiFN($labelpage));
246        $inli = 0;
247        $item = '';
248        foreach($instructions as $instruction) {
249            if($instruction[0] == 'listitem_open') {
250                $inli++;
251                continue;
252            }
253            if($inli === 1 && $instruction[0] == 'cdata') {
254                $item .= $instruction[1][0];
255            }
256            if($instruction[0] == 'listitem_close') {
257                $inli--;
258                if($inli === 0) {
259                    list($k, $v) = explode('=', $item, 2);
260                    $k = trim($k);
261                    $v = trim($v);
262                    if($k && $v) $labels[$k] = $v;
263                    $item = '';
264                }
265            }
266        }
267
268        // apply labels to all fields
269        $len = count($data['fields']);
270        for($i = 0; $i < $len; $i++) {
271            if(isset($data['fields'][$i]->depends_on)) {
272                // translate dependency on fieldsets
273                $label = $data['fields'][$i]->depends_on[0];
274                if(isset($labels[$label])) {
275                    $data['fields'][$i]->depends_on[0] = $labels[$label];
276                }
277
278            } else if(isset($data['fields'][$i]->opt['label'])) {
279                // translate field labels
280                $label = $data['fields'][$i]->opt['label'];
281                if(isset($labels[$label])) {
282                    $data['fields'][$i]->opt['display'] = $labels[$label];
283                }
284            }
285        }
286
287        if(isset($data['thanks'])) {
288            if(isset($labels[$data['thanks']])) {
289                $data['thanks'] = $labels[$data['thanks']];
290            }
291        }
292
293    }
294
295    /**
296     * Validate posted data, perform action(s)
297     *
298     * @param array $data all data passed to render()
299     * @return bool|string
300     *      returns thanks message when fields validated and performed the action(s) succesfully;
301     *      otherwise returns false.
302     */
303    private function _handlepost($data) {
304        $success = true;
305        foreach($data['fields'] as $index => $field) {
306            /** @var $field helper_plugin_bureaucracyau_field */
307
308            $isValid = true;
309            if($field->getFieldType() === 'file') {
310                $file = array();
311                foreach($_FILES['bureaucracyau'] as $key => $value) {
312                    $file[$key] = $value[$index];
313                }
314                $isValid = $field->handle_post($file, $data['fields'], $index, $this->form_id);
315
316            } elseif($field->getFieldType() === 'fieldset' || !$field->hidden) {
317                $isValid = $field->handle_post($_POST['bureaucracyau'][$index], $data['fields'], $index, $this->form_id);
318            }
319
320            if(!$isValid) {
321                // Do not return instantly to allow validation of all fields.
322                $success = false;
323            }
324        }
325        if(!$success) {
326            return false;
327        }
328
329        $thanks_array = array();
330
331        foreach($data['actions'] as $actionData) {
332            /** @var helper_plugin_bureaucracyau_action $action */
333            $action = $this->loadHelper($actionData['actionname'], false);
334
335            // action helper found?
336            if(!$action) {
337                msg(sprintf($this->getLang('e_unknownaction'), hsc($actionData['actionname'])), -1);
338                return false;
339            }
340
341            try {
342                $thanks_array[] = $action->run(
343                    $data['fields'],
344                    $data['thanks'],
345                    $actionData['argv']
346                );
347            } catch(Exception $e) {
348                msg($e->getMessage(), -1);
349                return false;
350            }
351        }
352
353        // Perform after_action hooks
354        foreach($data['fields'] as $field) {
355            $field->after_action();
356        }
357
358		// create thanks string
359		$thanks = implode('', array_unique($thanks_array));
360
361        return $thanks;
362    }
363
364    /**
365     * Create the form
366     *
367     * @param helper_plugin_bureaucracyau_field[] $fields array with form fields
368     * @return string html of the form
369     */
370    private function _htmlform($fields) {
371        global $ID;
372
373        $form = new Doku_Form(array('class'   => 'bureaucracyau__plugin',
374                                    'id'      => 'bureaucracyau__plugin' . $this->form_id,
375                                    'enctype' => 'multipart/form-data'));
376        $form->addHidden('id', $ID);
377        $form->addHidden('bureaucracyau[$$id]', $this->form_id);
378
379        foreach($fields as $id => $field) {
380            $field->renderfield(array('name' => 'bureaucracyau[' . $id . ']'), $form, $this->form_id);
381        }
382
383        return $form->getForm();
384    }
385
386    /**
387     * Parse a line into (quoted) arguments
388     * Splits line at spaces, except when quoted
389     *
390     * @author William Fletcher <wfletcher@applestone.co.za>
391     *
392     * @param string $line line to parse
393     * @param array  $lines all remaining lines
394     * @return array with all the arguments
395     */
396    private function _parse_line($line, &$lines) {
397        $args = array();
398        $inQuote = false;
399        $escapedQuote = false;
400        $arg = '';
401        do {
402            $len = strlen($line);
403            for($i = 0; $i < $len; $i++) {
404                if($line{$i} == '"') {
405                    if($inQuote) {
406                        if($escapedQuote) {
407                            $arg .= '"';
408                            $escapedQuote = false;
409                            continue;
410                        }
411                        if($line{$i + 1} == '"') {
412                            $escapedQuote = true;
413                            continue;
414                        }
415                        array_push($args, $arg);
416                        $inQuote = false;
417                        $arg = '';
418                        continue;
419                    } else {
420                        $inQuote = true;
421                        continue;
422                    }
423                } else if($line{$i} == ' ') {
424                    if($inQuote) {
425                        $arg .= ' ';
426                        continue;
427                    } else {
428                        if(strlen($arg) < 1) continue;
429                        array_push($args, $arg);
430                        $arg = '';
431                        continue;
432                    }
433                }
434                $arg .= $line{$i};
435            }
436            if(!$inQuote || count($lines) === 0) break;
437            $line = array_shift($lines);
438            $arg .= "\n";
439        } while(true);
440        if(strlen($arg) > 0) array_push($args, $arg);
441        return $args;
442    }
443
444    /**
445     * Clean class name
446     *
447     * @param string $classname
448     * @return string cleaned name
449     */
450    private function _sanitizeClassName($classname) {
451        return preg_replace('/[^\w\x7f-\xff]/', '', strtolower($classname));
452    }
453
454    /**
455     * Apply replacement patterns and values as prepared earlier
456     * (disable $strftime to prevent double replacements with default strftime() replacements in nstemplate)
457     *
458     * @param string $input    The text to work on
459     * @param bool   $strftime Apply strftime() replacements
460     * @return string processed text
461     */
462    function replace($input, $strftime = true) {
463        foreach ($this->values as $label => $value) {
464            $pattern = $this->patterns[$label];
465            if (is_callable($value)) {
466                $input = preg_replace_callback(
467                    $pattern,
468                    $value,
469                    $input
470                );
471            } else {
472                $input = preg_replace($pattern, $value, $input);
473            }
474
475        }
476
477        if($strftime) {
478            $input = preg_replace_callback(
479                '/%./',
480                function($m){return strftime($m[0]);},
481                $input
482            );
483        }
484        // user syntax: %%.(.*?)
485        // strftime() is already applied once, so syntax is at this point: %.(.*?)
486        $input = preg_replace_callback(
487            '/@DATE\((.*?)(?:,\s*(.*?))?\)@/',
488            array($this, 'replacedate'),
489            $input
490        );
491        return $input;
492    }
493
494    /**
495     * (callback) Replace date by request datestring
496     * e.g. '%m(30-11-1975)' is replaced by '11'
497     *
498     * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern
499     * @return string
500     */
501    function replacedate($match) {
502        global $conf;
503
504        //no 2nd argument for default date format
505        if($match[2] == null) {
506            $match[2] = $conf['dformat'];
507        }
508
509        return strftime($match[2], strtotime($match[1]));
510    }
511
512    /**
513     * Same replacements as applied at template namespaces
514     *
515     * @see parsePageTemplate()
516     */
517    function prepareNamespacetemplateReplacements() {
518        /* @var Input $INPUT */
519        global $INPUT;
520        global $USERINFO;
521        global $conf;
522        global $ID;
523
524        $this->patterns['__formpage_id__'] = '/@FORMPAGE_ID@/';
525        $this->patterns['__formpage_ns__'] = '/@FORMPAGE_NS@/';
526        $this->patterns['__formpage_curns__'] = '/@FORMPAGE_CURNS@/';
527        $this->patterns['__formpage_file__'] = '/@FORMPAGE_FILE@/';
528        $this->patterns['__formpage_!file__'] = '/@FORMPAGE_!FILE@/';
529        $this->patterns['__formpage_!file!__'] = '/@FORMPAGE_!FILE!@/';
530        $this->patterns['__formpage_page__'] = '/@FORMPAGE_PAGE@/';
531        $this->patterns['__formpage_!page__'] = '/@FORMPAGE_!PAGE@/';
532        $this->patterns['__formpage_!!page__'] = '/@FORMPAGE_!!PAGE@/';
533        $this->patterns['__formpage_!page!__'] = '/@FORMPAGE_!PAGE!@/';
534        $this->patterns['__user__'] = '/@USER@/';
535        $this->patterns['__name__'] = '/@NAME@/';
536        $this->patterns['__mail__'] = '/@MAIL@/';
537        $this->patterns['__date__'] = '/@DATE@/';
538
539        // replace placeholders
540        $file = noNS($ID);
541        $page = strtr($file, $conf['sepchar'], ' ');
542        $this->values['__formpage_id__'] = $ID;
543        $this->values['__formpage_ns__'] = curNS($ID);
544        $this->values['__formpage_curns__'] = getNS($ID);
545        $this->values['__formpage_file__'] = $file;
546        $this->values['__formpage_!file__'] = utf8_ucfirst($file);
547        $this->values['__formpage_!file!__'] = utf8_strtoupper($file);
548        $this->values['__formpage_page__'] = $page;
549        $this->values['__formpage_!page__'] = utf8_ucfirst($page);
550        $this->values['__formpage_!!page__'] = utf8_ucwords($page);
551        $this->values['__formpage_!page!__'] = utf8_strtoupper($page);
552        $this->values['__user__'] = $INPUT->server->str('REMOTE_USER');
553        $this->values['__name__'] = $USERINFO['name'];
554        $this->values['__mail__'] = $USERINFO['mail'];
555        $this->values['__date__'] = strftime($conf['dformat']);
556    }
557
558    /**
559     * Date time replacements
560     */
561    function prepareDateTimereplacements() {
562        $this->patterns['__year__'] = '/@YEAR@/';
563        $this->patterns['__month__'] = '/@MONTH@/';
564        $this->patterns['__monthname__'] = '/@MONTHNAME@/';
565        $this->patterns['__day__'] = '/@DAY@/';
566        $this->patterns['__time__'] = '/@TIME@/';
567        $this->patterns['__timesec__'] = '/@TIMESEC@/';
568        $this->values['__year__'] = date('Y');
569        $this->values['__month__'] = date('m');
570        $this->values['__monthname__'] = date('B');
571        $this->values['__day__'] = date('d');
572        $this->values['__time__'] = date('H:i');
573        $this->values['__timesec__'] = date('H:i:s');
574
575    }
576}
577