xref: /plugin/struct/types/AbstractBaseType.php (revision ecc947547e297aac8666c036f265ff044e632a3e)
1<?php
2namespace dokuwiki\plugin\struct\types;
3
4use dokuwiki\plugin\struct\meta\Column;
5use dokuwiki\plugin\struct\meta\StructException;
6use dokuwiki\plugin\struct\meta\ValidationException;
7
8/**
9 * Class AbstractBaseType
10 *
11 * This class represents a basic type that can be configured to be used in a Schema. It is the main
12 * part of a column definition as defined in meta\Column
13 *
14 * This defines also how the content of the coulmn will be entered and formatted.
15 *
16 * @package dokuwiki\plugin\struct\types
17 * @see Column
18 */
19abstract class AbstractBaseType {
20
21    /**
22     * @var array current config
23     */
24    protected $config = array();
25
26    /**
27     * @var array config keys that should not be cleaned despite not being in $config
28     */
29    protected $keepconfig = array('label', 'hint', 'visibility');
30
31    /**
32     * @var string label for the field
33     */
34    protected $label = '';
35
36    /**
37     * @var bool is this a multivalue field?
38     */
39    protected $ismulti = false;
40
41    /**
42     * @var int the type ID
43     */
44    protected $tid = 0;
45
46    /**
47     * @var null|Column the column context this type is part of
48     */
49    protected $context = null;
50
51    /**
52     * @var \DokuWiki_Plugin
53     */
54    protected $hlp = null;
55
56    /**
57     * AbstractBaseType constructor.
58     * @param array|null $config The configuration, might be null if nothing saved, yet
59     * @param string $label The label for this field (empty for new definitions=
60     * @param bool $ismulti Should this field accept multiple values?
61     * @param int $tid The id of this type if it has been saved, yet
62     */
63    public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) {
64        // general config options
65        $baseconfig = array(
66            'visibility' => array(
67                'inpage' => true,
68                'ineditor' => true,
69            )
70        );
71
72        // use previously saved configuration, ignoring all keys that are not supposed to be here
73        if(!is_null($config)) {
74            $this->mergeConfig($config, $this->config);
75        }
76
77        $this->initTransConfig();
78        $this->config = array_merge($baseconfig, $this->config);
79        $this->label = $label;
80        $this->ismulti = (bool) $ismulti;
81        $this->tid = $tid;
82    }
83
84    /**
85     * Merge the current config with the base config of the type
86     *
87     * Ignores all keys that are not supposed to be there. Recurses into sub keys
88     *
89     * @param array $current Current configuration
90     * @param array $config Base Type configuration
91     */
92    protected function mergeConfig($current, &$config) {
93        foreach($current as $key => $value) {
94            if(isset($config[$key]) || in_array($key, $this->keepconfig)) {
95                if(is_array($config[$key])) {
96                    $this->mergeConfig($value, $config[$key]);
97                } else {
98                    $config[$key] = $value;
99                }
100            }
101        }
102    }
103
104    /**
105     * Add the translatable keys to the configuration
106     *
107     * This checks if a configuration for the translation plugin exists and if so
108     * adds all configured languages to the config array. This ensures all types
109     * can have translatable labels.
110     */
111    protected function initTransConfig() {
112        global $conf;
113        $lang = $conf['lang'];
114        if(isset($conf['plugin']['translation']['translations'])) {
115            $lang .= ' ' . $conf['plugin']['translation']['translations'];
116        }
117        $langs = explode(' ', $lang);
118        $langs = array_map('trim', $langs);
119        $langs = array_filter($langs);
120        $langs = array_unique($langs);
121
122        if(!isset($this->config['label'])) $this->config['label'] = array();
123        if(!isset($this->config['hint'])) $this->config['hint'] = array();
124        // initialize missing keys
125        foreach($langs as $lang) {
126            if(!isset($this->config['label'][$lang])) $this->config['label'][$lang] = '';
127            if(!isset($this->config['hint'][$lang])) $this->config['hint'][$lang] = '';
128        }
129        // strip unknown languages
130        foreach(array_keys($this->config['label']) as $key) {
131            if(!in_array($key, $langs)) unset($this->config['label'][$key]);
132        }
133        foreach(array_keys($this->config['hint']) as $key) {
134            if(!in_array($key, $langs)) unset($this->config['hint'][$key]);
135        }
136
137    }
138
139    /**
140     * Returns data as associative array
141     *
142     * @return array
143     */
144    public function getAsEntry() {
145        return array(
146            'config' => json_encode($this->config),
147            'label' => $this->label,
148            'ismulti' => $this->ismulti,
149            'class' => $this->getClass()
150        );
151    }
152
153    /**
154     * The class name of this type (no namespace)
155     * @return string
156     */
157    public function getClass() {
158        $class = get_class($this);
159        return substr($class, strrpos($class, "\\") + 1);
160    }
161
162    /**
163     * Return the current configuration for this type
164     *
165     * @return array
166     */
167    public function getConfig() {
168        return $this->config;
169    }
170
171    /**
172     * @return boolean
173     */
174    public function isMulti() {
175        return $this->ismulti;
176    }
177
178    /**
179     * @return string
180     */
181    public function getLabel() {
182        return $this->label;
183    }
184
185    /**
186     * Returns the translated label for this type
187     *
188     * Uses the current language as determined by $conf['lang']. Falls back to english
189     * and then to the Schema label
190     *
191     * @return string
192     */
193    public function getTranslatedLabel() {
194        global $conf;
195        $lang = $conf['lang'];
196        if(!blank($this->config['label'][$lang])) {
197            return $this->config['label'][$lang];
198        }
199        if(!blank($this->config['label']['en'])) {
200            return $this->config['label']['en'];
201        }
202        return $this->label;
203    }
204
205    /**
206     * Returns the translated hint for this type
207     *
208     * Uses the current language as determined by $conf['lang']. Falls back to english.
209     * Returns empty string if no hint is configured
210     *
211     * @return string
212     */
213    public function getTranslatedHint() {
214        global $conf;
215        $lang = $conf['lang'];
216        if(!blank($this->config['hint'][$lang])) {
217            return $this->config['hint'][$lang];
218        }
219        if(!blank($this->config['hint']['en'])) {
220            return $this->config['hint']['en'];
221        }
222        return '';
223    }
224
225    /**
226     * @return int
227     */
228    public function getTid() {
229        return $this->tid;
230    }
231
232    /**
233     * @throws StructException
234     * @return Column
235     */
236    public function getContext() {
237        if(is_null($this->context))
238            throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.');
239        return $this->context;
240    }
241
242    /**
243     * @param Column $context
244     */
245    public function setContext($context) {
246        $this->context = $context;
247    }
248
249    /**
250     * @return bool
251     */
252    public function isVisibleInEditor() {
253        return $this->config['visibility']['ineditor'];
254    }
255
256    /**
257     * @return bool
258     */
259    public function isVisibleInPage() {
260        return $this->config['visibility']['inpage'];
261    }
262
263    /**
264     * Split a single value into multiple values
265     *
266     * This function is called on saving data when only a single value instead of an array
267     * was submitted.
268     *
269     * Types implementing their own @see multiValueEditor() will probably want to override this
270     *
271     * @param string $value
272     * @return array
273     */
274    public function splitValues($value) {
275        return array_map('trim', explode(',', $value));
276    }
277
278    /**
279     * Return the editor to edit multiple values
280     *
281     * Types can override this to provide a better alternative than multiple entry fields
282     *
283     * @param string $name the form base name where this has to be stored
284     * @param string[] $values the current values
285     * @return string html
286     */
287    public function multiValueEditor($name, $values) {
288        $html = '';
289        foreach($values as $value) {
290            $html .= '<div class="multiwrap">';
291            $html .= $this->valueEditor($name . '[]', $value);
292            $html .= '</div>';
293        }
294        // empty field to add
295        $html .= '<div class="newtemplate">';
296        $html .= '<div class="multiwrap">';
297        $html .= $this->valueEditor($name . '[]', '');
298        $html .= '</div>';
299        $html .= '</div>';
300
301        return $html;
302    }
303
304    /**
305     * Return the editor to edit a single value
306     *
307     * @param string $name the form name where this has to be stored
308     * @param string $value the current value
309     * @return string html
310     */
311    public function valueEditor($name, $value) {
312        $class = 'struct_' . strtolower($this->getClass());
313
314        // support the autocomplete configurations out of the box
315        if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) {
316            $class .= ' struct_autocomplete';
317        }
318
319        $name = hsc($name);
320        $value = hsc($value);
321        $html = "<input name=\"$name\" value=\"$value\" class=\"$class\" />";
322        return "$html";
323    }
324
325    /**
326     * Output the stored data
327     *
328     * @param string|int $value the value stored in the database
329     * @param \Doku_Renderer $R the renderer currently used to render the data
330     * @param string $mode The mode the output is rendered in (eg. XHTML)
331     * @return bool true if $mode could be satisfied
332     */
333    public function renderValue($value, \Doku_Renderer $R, $mode) {
334        $R->cdata($value);
335        return true;
336    }
337
338    /**
339     * format and return the data
340     *
341     * @param int[]|string[] $values the values stored in the database
342     * @param \Doku_Renderer $R the renderer currently used to render the data
343     * @param string $mode The mode the output is rendered in (eg. XHTML)
344     * @return bool true if $mode could be satisfied
345     */
346    public function renderMultiValue($values, \Doku_Renderer $R, $mode) {
347        $len = count($values);
348        for($i = 0; $i < $len; $i++) {
349            $this->renderValue($values[$i], $R, $mode);
350            if($i < $len - 1) {
351                $R->cdata(', ');
352            }
353        }
354        return true;
355    }
356
357    /**
358     * This function builds a where clause for this column, comparing
359     * the current value stored in $column with $value. Types can use it to do
360     * clever things with the comparison.
361     *
362     * This default implementation is probably good enough for most basic types
363     *
364     * @param string $column The column name to us in the SQL
365     * @param string $comp The comparator @see Search::$COMPARATORS
366     * @param string $value
367     * @return array Tuple with the SQL and parameter array
368     */
369    public function compare($column, $comp, $value) {
370        switch($comp) {
371            case '~':
372                $sql = "$column LIKE ?";
373                $opt = array($value);
374                break;
375            case '!~':
376                $sql = "$column NOT LIKE ?";
377                $opt = array($value);
378                break;
379            default:
380                $sql = "$column $comp ?";
381                $opt = array($value);
382        }
383
384        return array($sql, $opt);
385    }
386
387    /**
388     * Validate and optionally clean a single value
389     *
390     * This function needs to throw a validation exception when validation fails.
391     * The exception message will be prefixed by the appropriate field on output
392     *
393     * The function should return the value as it should be saved later on.
394     *
395     * @param string|int $value
396     * @return int|string the cleaned value
397     * @throws ValidationException
398     */
399    public function validate($value) {
400        return trim($value);
401    }
402
403    /**
404     * Overwrite to handle Ajax requests
405     *
406     * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will
407     * be redirected to this function on a fully initialized type. The result is
408     * JSON encoded and returned to the caller. Access additional parameter via $INPUT
409     * as usual
410     *
411     * @throws StructException when something goes wrong
412     * @return mixed
413     */
414    public function handleAjax() {
415        throw new StructException('not implemented');
416    }
417
418    /**
419     * Convenience method to access plugin language strings
420     *
421     * @param string $string
422     * @return string
423     */
424    public function getLang($string) {
425        if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct');
426        return $this->hlp->getLang($string);
427    }
428}
429