1<?php
2
3/**
4 * Base class for form fields
5 *
6 * This class provides basic functionality for many form fields. It supports
7 * labels, basic validation and template-based XHTML output.
8 *
9 * @author Adrian Lang <lang@cosmocode.de>
10 **/
11
12/**
13 * Class helper_plugin_bureaucracy_field
14 *
15 * base class for all the form fields
16 */
17class helper_plugin_bureaucracy_field extends syntax_plugin_bureaucracy {
18
19    protected $mandatory_args = 2;
20    public $opt = array();
21    /** @var string|array */
22    protected $tpl;
23    protected $checks = array();
24    public $hidden = false;
25    protected $error = false;
26    protected $checktypes = array(
27        '/' => 'match',
28        '<' => 'max',
29        '>' => 'min'
30    );
31
32    /**
33     * Construct a helper_plugin_bureaucracy_field object
34     *
35     * This constructor initializes a helper_plugin_bureaucracy_field object
36     * based on a given definition.
37     *
38     * The first two items represent:
39     *   * the type of the field
40     *   * and the label the field has been given.
41     * Additional arguments are type-specific mandatory extra arguments and optional arguments.
42     *
43     * The optional arguments may add constraints to the field value, provide a
44     * default value, mark the field as optional or define that the field is
45     * part of a pagename (when using the template action).
46     *
47     * Since the field objects are cached, this constructor may not reference
48     * request data.
49     *
50     * @param array $args The tokenized definition, only split at spaces
51     */
52    public function initialize($args) {
53        $this->init($args);
54        $this->standardArgs($args);
55    }
56
57    /**
58     * Return false to prevent DokuWiki reusing instances of the plugin
59     *
60     * @return bool
61     */
62    public function isSingleton() {
63        return false;
64    }
65
66    /**
67     * Checks number of arguments and store 'cmd', 'label' and 'display' values
68     *
69     * @param array $args array with the definition
70     */
71    protected function init(&$args) {
72        if(count($args) < $this->mandatory_args){
73            msg(sprintf($this->getLang('e_missingargs'), hsc($args[0]),
74                        hsc($args[1])), -1);
75            return;
76        }
77
78        // get standard arguments
79        $this->opt = array();
80        foreach (array('cmd', 'label') as $key) {
81            if (count($args) === 0) break;
82            $this->opt[$key] = array_shift($args);
83        }
84        $this->opt['display'] = $this->opt['label']; // allow to modify display value independently
85    }
86
87    /**
88     * Check for additional arguments and store their values
89     *
90     * @param array $args array with remaining definition arguments
91     */
92    protected function standardArgs($args) {
93        // parse additional arguments
94        foreach($args as $arg){
95            if ($arg[0] == '=') {
96                $this->setVal(substr($arg,1));
97            } elseif ($arg == '!') {
98                $this->opt['optional'] = true;
99            } elseif ($arg == '^') {
100                //only one field has focus
101                if (helper_plugin_bureaucracy_field::hasFocus()) {
102                    $this->opt['id'] = 'focus__this';
103                }
104            } elseif($arg == '@') {
105                $this->opt['pagename'] = true;
106            } elseif($arg == '@@') {
107                $this->opt['replyto'] = true;
108            } elseif(preg_match('/x\d/', $arg)) {
109                $this->opt['rows'] = substr($arg,1);
110            } elseif($arg[0] == '.') {
111                $this->opt['class'] = substr($arg, 1);
112            } elseif(preg_match('/^0{2,}$/', $arg)) {
113                $this->opt['leadingzeros'] = strlen($arg);
114            } elseif($arg[0].$arg[1] == '**') {
115                $this->opt['matchexplanation'] = substr($arg,2);
116            } else {
117                $t = $arg[0];
118                $d = substr($arg,1);
119                if (in_array($t, array('>', '<')) && !is_numeric($d)) {
120                    break;
121                }
122                if ($t == '/') {
123                    if (substr($d, -1) !== '/') {
124                        break;
125                    }
126                    $d = substr($d, 0, -1);
127                }
128                if (!isset($this->checktypes[$t]) || !method_exists($this, 'validate_' . $this->checktypes[$t])) {
129                    msg(sprintf($this->getLang('e_unknownconstraint'), hsc($t).' ('.hsc($arg).')'), -1);
130                    return;
131                }
132                $this->checks[] = array('t' => $t, 'd' => $d);
133            }
134        }
135    }
136
137    /**
138     * Add parsed element to Form which generates XHTML
139     *
140     * Outputs the represented field using the passed Doku_Form object.
141     * Additional parameters (CSS class & HTML name) are passed in $params.
142     * HTML output is created by passing the template $this->tpl to the simple
143     * template engine _parse_tpl.
144     *
145     * @param array     $params Additional HTML specific parameters
146     * @param Doku_Form $form   The target Doku_Form object
147     * @param int       $formid unique identifier of the form which contains this field
148     */
149    public function renderfield($params, Doku_Form $form, $formid) {
150        $this->_handlePreload();
151        if(!$form->_infieldset){
152            $form->startFieldset('');
153        }
154        if ($this->error) {
155            $params['class'] = 'bureaucracy_error';
156        }
157
158        $params = array_merge($this->opt, $params);
159        $form->addElement($this->_parse_tpl($this->tpl, $params));
160    }
161
162    /**
163     * Only the first use get the focus, next calls not
164     *
165     * @return bool
166     */
167    protected static function hasFocus(){
168        static $focus = true;
169        if($focus) {
170            $focus = false;
171            return true;
172        } else {
173            return false;
174        }
175    }
176
177
178    /**
179     * Check for preload value in the request url
180     */
181    protected function _handlePreload() {
182        $preload_name = '@' . strtr($this->getParam('label'),' .','__') . '@';
183        if (isset($_GET[$preload_name])) {
184            $this->setVal($_GET[$preload_name]);
185        }
186    }
187
188    /**
189     * Handle a post to the field
190     *
191     * Accepts and validates a posted value.
192     *
193     * (Overridden by fieldset, which has as argument an array with the form array by reference)
194     *
195     * @param string $value  The passed value or array or null if none given
196     * @param helper_plugin_bureaucracy_field[] $fields (reference) form fields (POST handled upto $this field)
197     * @param int    $index  index number of field in form
198     * @param int    $formid unique identifier of the form which contains this field
199     * @return bool Whether the passed value is valid
200     */
201    public function handle_post($value, &$fields, $index, $formid) {
202        return $this->hidden || $this->setVal($value);
203    }
204
205    /**
206     * Get the field type
207     *
208     * @return string
209     **/
210    public function getFieldType() {
211        return $this->opt['cmd'];
212    }
213
214    /**
215     * Get the replacement pattern used by action
216     *
217     * @return string
218     */
219    public function getReplacementPattern() {
220        $label = $this->getParam('label');
221        $value = $this->getParam('value');
222
223        if (is_array($value)) {
224            return '/(@@|##)' . preg_quote($label, '/') .
225                '(?:\((?P<delimiter>.*?)\))?' .//delimiter
226                '(?:\|(?P<default>.*?))' . (count($value) == 0 ? '' : '?') .
227                '\1/si';
228        }
229
230        return '/(@@|##)' . preg_quote($label, '/') .
231            '(?:\|(.*?))' . (is_null($value) ? '' : '?') .
232            '\1/si';
233    }
234
235    /**
236     * Used as an callback for preg_replace_callback
237     *
238     * @param $matches
239     * @return string
240     */
241    public function replacementMultiValueCallback($matches) {
242        $value = $this->opt['value'];
243
244        //default value
245        if (is_null($value) || $value === false) {
246            if (isset($matches['default']) && $matches['default'] != '') {
247                return $matches['default'];
248            }
249            return $matches[0];
250        }
251
252        //check if matched string containts a pair of brackets
253        $delimiter = preg_match('/\(.*\)/s', $matches[0]) ? $matches['delimiter'] : ', ';
254
255        return implode($delimiter, $value);
256    }
257
258    /**
259     * Get the value used by action
260     * If value is a callback preg_replace_callback is called instead preg_replace
261     *
262     * @return mixed|string
263     */
264    public function getReplacementValue() {
265        $value = $this->getParam('value');
266
267        if (is_array($value)) {
268            return array($this, 'replacementMultiValueCallback');
269        }
270
271        return is_null($value) || $value === false ? '$2' : $value;
272    }
273
274    /**
275     * Validate value and stores it
276     *
277     * @param mixed $value value entered into field
278     * @return bool whether the passed value is valid
279     */
280    protected function setVal($value) {
281        if ($value === '') {
282            $value = null;
283        }
284        $this->opt['value'] = $value;
285        try {
286            $this->_validate();
287            $this->error = false;
288        } catch (Exception $e) {
289            msg($e->getMessage(), -1);
290            $this->error = true;
291        }
292        return !$this->error;
293    }
294
295    /**
296     * Whether the field is true (used for depending fieldsets)
297     *
298     * @return bool whether field is set
299     */
300    public function isSet_() {
301        return !is_null($this->getParam('value'));
302    }
303
304    /**
305     * Validate value of field and throws exceptions for bad values.
306     *
307     * @throws Exception when field didn't validate.
308     */
309    protected function _validate() {
310        $value = $this->getParam('value');
311        if (is_null($value)) {
312            if(!isset($this->opt['optional'])) {
313                throw new Exception(sprintf($this->getLang('e_required'),hsc($this->opt['label'])));
314            }
315            return;
316        }
317
318        foreach ($this->checks as $check) {
319            $checktype = $this->checktypes[$check['t']];
320            if (!call_user_func(array($this, 'validate_' . $checktype), $check['d'], $value)) {
321                //replacement is custom explanation or just the regexp or the requested value
322                if(isset($this->opt['matchexplanation'])) {
323                    $replacement = hsc($this->opt['matchexplanation']);
324                } elseif($checktype == 'match') {
325                    $replacement = sprintf($this->getLang('checkagainst'), hsc($check['d']));
326                } else {
327                    $replacement = hsc($check['d']);
328                }
329
330                throw new Exception(sprintf($this->getLang('e_' . $checktype), hsc($this->opt['label']), $replacement));
331            }
332        }
333    }
334
335    /**
336     * Get an arbitrary parameter
337     *
338     * @param string $name
339     * @return mixed|null
340     */
341    public function getParam($name) {
342        if (!isset($this->opt[$name]) || $name === 'value' && $this->hidden) {
343            return null;
344        }
345        if ($name === 'pagename') {
346            // If $this->opt['pagename'] is set, return the escaped value of the field.
347            $value = $this->getParam('value');
348            if (is_null($value)) {
349                return null;
350            }
351            global $conf;
352            if($conf['useslash']) $value = str_replace('/',' ',$value);
353            return str_replace(':',' ',$value);
354        }
355        return $this->opt[$name];
356    }
357
358    /**
359     * Parse a template with given parameters
360     *
361     * Replaces variables specified like @@VARNAME|default@@ using the passed
362     * value map.
363     *
364     * @param string|array $tpl    The template as string or array
365     * @param array        $params A hash mapping parameters to values
366     *
367     * @return string|array The parsed template
368     */
369    protected function _parse_tpl($tpl, $params) {
370        // addElement supports a special array format as well. In this case
371        // not all elements should be escaped.
372        $is_simple = !is_array($tpl);
373        if ($is_simple) $tpl = array($tpl);
374
375        foreach ($tpl as &$val) {
376            // Select box passes options as an array. We do not escape those.
377            if (is_array($val)) continue;
378
379            // find all variables and their defaults or param values
380            preg_match_all('/@@([A-Z]+)(?:\|((?:[^@]|@$|@[^@])*))?@@/', $val, $pregs);
381            for ($i = 0 ; $i < count($pregs[2]) ; ++$i) {
382                if (isset($params[strtolower($pregs[1][$i])])) {
383                    $pregs[2][$i] = $params[strtolower($pregs[1][$i])];
384                }
385            }
386            // we now have placeholders in $pregs[0] and their values in $pregs[2]
387            $replacements = array(); // check if empty to prevent php 5.3 warning
388            if (!empty($pregs[0])) {
389                $replacements = array_combine($pregs[0], $pregs[2]);
390            }
391
392            if($is_simple){
393                // for simple string templates, we escape all replacements
394                $replacements = array_map('hsc', $replacements);
395            }else{
396                // for the array ones, we escape the label and display only
397                if(isset($replacements['@@LABEL@@']))   $replacements['@@LABEL@@']   = hsc($replacements['@@LABEL@@']);
398                if(isset($replacements['@@DISPLAY@@'])) $replacements['@@DISPLAY@@'] = hsc($replacements['@@DISPLAY@@']);
399            }
400
401            // we attach a mandatory marker to the display
402            if(isset($replacements['@@DISPLAY@@']) && !isset($params['optional'])){
403                $replacements['@@DISPLAY@@'] .= ' <sup>*</sup>';
404            }
405            $val = str_replace(array_keys($replacements), array_values($replacements), $val);
406        }
407        return $is_simple ? $tpl[0] : $tpl;
408    }
409
410    /**
411     * Executed after performing the action hooks
412     */
413    public function after_action() {
414    }
415
416    /**
417     * Constraint function: value of field should match this regexp
418     *
419     * @param string $d regexp
420     * @param mixed $value
421     * @return int|bool
422     */
423    protected function validate_match($d, $value) {
424        return @preg_match('/' . $d . '/i', $value);
425    }
426
427    /**
428     * Constraint function: value of field should be bigger
429     *
430     * @param int|number $d lower bound
431     * @param mixed $value of field
432     * @return bool
433     */
434    protected function validate_min($d, $value) {
435        return $value > $d;
436    }
437
438    /**
439     * Constraint function: value of field should be smaller
440     *
441     * @param int|number $d upper bound
442     * @param mixed $value of field
443     * @return bool
444     */
445    protected function validate_max($d, $value) {
446        return $value < $d;
447    }
448
449    /**
450     * Available methods
451     *
452     * @return array
453     */
454    public function getMethods() {
455        $result = array();
456        $result[] = array(
457            'name' => 'initialize',
458            'desc' => 'Initiate object, first parameters are at least cmd and label',
459            'params' => array(
460                'params' => 'array'
461            )
462        );
463        $result[] = array(
464            'name' => 'renderfield',
465            'desc' => 'Add parsed element to Form which generates XHTML',
466            'params' => array(
467                'params' => 'array',
468                'form' => 'Doku_Form',
469                'formid' => 'integer'
470            )
471        );
472        $result[] = array(
473            'name' => 'handle_post',
474            'desc' => 'Handle a post to the field',
475            'params' => array(
476                'value' => 'array',
477                'fields' => 'helper_plugin_bureaucracy_field[]',
478                'index' => 'Doku_Form',
479                'formid' => 'integer'
480            ),
481            'return' => array('isvalid' => 'bool')
482        );
483        $result[] = array(
484            'name' => 'getFieldType',
485            'desc' => 'Get the field type',
486            'return' => array('fieldtype' => 'string')
487        );
488        $result[] = array(
489            'name' => 'isSet_',
490            'desc' => 'Whether the field is true (used for depending fieldsets)  ',
491            'return' => array('isset' => 'bool')
492        );
493        $result[] = array(
494            'name' => 'getParam',
495            'desc' => 'Get an arbitrary parameter',
496            'params' => array(
497                'name' => 'string'
498            ),
499            'return' => array('Parameter value' => 'mixed|null')
500        );
501        $result[] = array(
502            'name' => 'after_action',
503            'desc' => 'Executed after performing the action hooks'
504        );
505        return $result;
506    }
507
508}
509