xref: /plugin/struct/types/AbstractBaseType.php (revision 9e7e1786988583727497586e3a20adf15a87a4d5)
1<?php
2namespace dokuwiki\plugin\struct\types;
3
4use dokuwiki\plugin\struct\meta\Column;
5use dokuwiki\plugin\struct\meta\QueryBuilder;
6use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
7use dokuwiki\plugin\struct\meta\StructException;
8use dokuwiki\plugin\struct\meta\ValidationException;
9use dokuwiki\plugin\struct\meta\Value;
10
11/**
12 * Class AbstractBaseType
13 *
14 * This class represents a basic type that can be configured to be used in a Schema. It is the main
15 * part of a column definition as defined in meta\Column
16 *
17 * This defines also how the content of the coulmn will be entered and formatted.
18 *
19 * @package dokuwiki\plugin\struct\types
20 * @see Column
21 */
22abstract class AbstractBaseType {
23
24    /**
25     * @var array current config
26     */
27    protected $config = array();
28
29    /**
30     * @var array config keys that should not be cleaned despite not being in $config
31     */
32    protected $keepconfig = array('label', 'hint', 'visibility');
33
34    /**
35     * @var string label for the field
36     */
37    protected $label = '';
38
39    /**
40     * @var bool is this a multivalue field?
41     */
42    protected $ismulti = false;
43
44    /**
45     * @var int the type ID
46     */
47    protected $tid = 0;
48
49    /**
50     * @var null|Column the column context this type is part of
51     */
52    protected $context = null;
53
54    /**
55     * @var \DokuWiki_Plugin
56     */
57    protected $hlp = null;
58
59    /**
60     * AbstractBaseType constructor.
61     * @param array|null $config The configuration, might be null if nothing saved, yet
62     * @param string $label The label for this field (empty for new definitions=
63     * @param bool $ismulti Should this field accept multiple values?
64     * @param int $tid The id of this type if it has been saved, yet
65     */
66    public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) {
67        // general config options
68        $baseconfig = array(
69            'visibility' => array(
70                'inpage' => true,
71                'ineditor' => true,
72            )
73        );
74
75        // use previously saved configuration, ignoring all keys that are not supposed to be here
76        if(!is_null($config)) {
77            $this->mergeConfig($config, $this->config);
78        }
79
80        $this->initTransConfig();
81        $this->config = array_merge($baseconfig, $this->config);
82        $this->label = $label;
83        $this->ismulti = (bool) $ismulti;
84        $this->tid = $tid;
85    }
86
87    /**
88     * Merge the current config with the base config of the type
89     *
90     * Ignores all keys that are not supposed to be there. Recurses into sub keys
91     *
92     * @param array $current Current configuration
93     * @param array $config Base Type configuration
94     */
95    protected function mergeConfig($current, &$config) {
96        foreach($current as $key => $value) {
97            if(isset($config[$key]) || in_array($key, $this->keepconfig)) {
98                if(is_array($config[$key])) {
99                    $this->mergeConfig($value, $config[$key]);
100                } else {
101                    $config[$key] = $value;
102                }
103            }
104        }
105    }
106
107    /**
108     * Add the translatable keys to the configuration
109     *
110     * This checks if a configuration for the translation plugin exists and if so
111     * adds all configured languages to the config array. This ensures all types
112     * can have translatable labels.
113     */
114    protected function initTransConfig() {
115        global $conf;
116        $lang = $conf['lang'];
117        if(isset($conf['plugin']['translation']['translations'])) {
118            $lang .= ' ' . $conf['plugin']['translation']['translations'];
119        }
120        $langs = explode(' ', $lang);
121        $langs = array_map('trim', $langs);
122        $langs = array_filter($langs);
123        $langs = array_unique($langs);
124
125        if(!isset($this->config['label'])) $this->config['label'] = array();
126        if(!isset($this->config['hint'])) $this->config['hint'] = array();
127        // initialize missing keys
128        foreach($langs as $lang) {
129            if(!isset($this->config['label'][$lang])) $this->config['label'][$lang] = '';
130            if(!isset($this->config['hint'][$lang])) $this->config['hint'][$lang] = '';
131        }
132        // strip unknown languages
133        foreach(array_keys($this->config['label']) as $key) {
134            if(!in_array($key, $langs)) unset($this->config['label'][$key]);
135        }
136        foreach(array_keys($this->config['hint']) as $key) {
137            if(!in_array($key, $langs)) unset($this->config['hint'][$key]);
138        }
139
140    }
141
142    /**
143     * Returns data as associative array
144     *
145     * @return array
146     */
147    public function getAsEntry() {
148        return array(
149            'config' => json_encode($this->config),
150            'label' => $this->label,
151            'ismulti' => $this->ismulti,
152            'class' => $this->getClass()
153        );
154    }
155
156    /**
157     * The class name of this type (no namespace)
158     * @return string
159     */
160    public function getClass() {
161        $class = get_class($this);
162        return substr($class, strrpos($class, "\\") + 1);
163    }
164
165    /**
166     * Return the current configuration for this type
167     *
168     * @return array
169     */
170    public function getConfig() {
171        return $this->config;
172    }
173
174    /**
175     * @return boolean
176     */
177    public function isMulti() {
178        return $this->ismulti;
179    }
180
181    /**
182     * @return string
183     */
184    public function getLabel() {
185        return $this->label;
186    }
187
188    /**
189     * Returns the translated label for this type
190     *
191     * Uses the current language as determined by $conf['lang']. Falls back to english
192     * and then to the Schema label
193     *
194     * @return string
195     */
196    public function getTranslatedLabel() {
197        global $conf;
198        $lang = $conf['lang'];
199        if(!blank($this->config['label'][$lang])) {
200            return $this->config['label'][$lang];
201        }
202        if(!blank($this->config['label']['en'])) {
203            return $this->config['label']['en'];
204        }
205        return $this->label;
206    }
207
208    /**
209     * Returns the translated hint for this type
210     *
211     * Uses the current language as determined by $conf['lang']. Falls back to english.
212     * Returns empty string if no hint is configured
213     *
214     * @return string
215     */
216    public function getTranslatedHint() {
217        global $conf;
218        $lang = $conf['lang'];
219        if(!blank($this->config['hint'][$lang])) {
220            return $this->config['hint'][$lang];
221        }
222        if(!blank($this->config['hint']['en'])) {
223            return $this->config['hint']['en'];
224        }
225        return '';
226    }
227
228    /**
229     * @return int
230     */
231    public function getTid() {
232        return $this->tid;
233    }
234
235    /**
236     * @throws StructException
237     * @return Column
238     */
239    public function getContext() {
240        if(is_null($this->context))
241            throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.');
242        return $this->context;
243    }
244
245    /**
246     * @param Column $context
247     */
248    public function setContext($context) {
249        $this->context = $context;
250    }
251
252    /**
253     * @return bool
254     */
255    public function isVisibleInEditor() {
256        return $this->config['visibility']['ineditor'];
257    }
258
259    /**
260     * @return bool
261     */
262    public function isVisibleInPage() {
263        return $this->config['visibility']['inpage'];
264    }
265
266    /**
267     * Split a single value into multiple values
268     *
269     * This function is called on saving data when only a single value instead of an array
270     * was submitted.
271     *
272     * Types implementing their own @see multiValueEditor() will probably want to override this
273     *
274     * @param string $value
275     * @return array
276     */
277    public function splitValues($value) {
278        return array_map('trim', explode(',', $value));
279    }
280
281    /**
282     * Return the editor to edit multiple values
283     *
284     * Types can override this to provide a better alternative than multiple entry fields
285     *
286     * @param string $name the form base name where this has to be stored
287     * @param string[] $rawvalues the current values
288     * @return string html
289     */
290    public function multiValueEditor($name, $rawvalues) {
291        $html = '';
292        foreach($rawvalues as $value) {
293            $html .= '<div class="multiwrap">';
294            $html .= $this->valueEditor($name . '[]', $value);
295            $html .= '</div>';
296        }
297        // empty field to add
298        $html .= '<div class="newtemplate">';
299        $html .= '<div class="multiwrap">';
300        $html .= $this->valueEditor($name . '[]', '');
301        $html .= '</div>';
302        $html .= '</div>';
303
304        return $html;
305    }
306
307    /**
308     * Return the editor to edit a single value
309     *
310     * @param string $name  the form name where this has to be stored
311     * @param string $rawvalue the current value
312     * @return string html
313     */
314    public function valueEditor($name, $rawvalue) {
315        $class = 'struct_' . strtolower($this->getClass());
316
317        // support the autocomplete configurations out of the box
318        if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) {
319            $class .= ' struct_autocomplete';
320        }
321
322        $name = hsc($name);
323        $rawvalue = hsc($rawvalue);
324        $html = "<input name=\"$name\" value=\"$rawvalue\" class=\"$class\" />";
325        return "$html";
326    }
327
328    /**
329     * Output the stored data
330     *
331     * @param string|int $value the value stored in the database
332     * @param \Doku_Renderer $R the renderer currently used to render the data
333     * @param string $mode The mode the output is rendered in (eg. XHTML)
334     * @return bool true if $mode could be satisfied
335     */
336    public function renderValue($value, \Doku_Renderer $R, $mode) {
337        $value = $this->displayValue($value);
338        $R->cdata($value);
339        return true;
340    }
341
342    /**
343     * format and return the data
344     *
345     * @param int[]|string[] $values the values stored in the database
346     * @param \Doku_Renderer $R the renderer currently used to render the data
347     * @param string $mode The mode the output is rendered in (eg. XHTML)
348     * @return bool true if $mode could be satisfied
349     */
350    public function renderMultiValue($values, \Doku_Renderer $R, $mode) {
351        $len = count($values);
352        for($i = 0; $i < $len; $i++) {
353            $this->renderValue($values[$i], $R, $mode);
354            if($i < $len - 1) {
355                $R->cdata(', ');
356            }
357        }
358        return true;
359    }
360
361    /**
362     * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary.
363     *
364     * @param string|int $value the value stored in the database
365     * @param \Doku_Renderer $R the renderer currently used to render the data
366     * @param string $mode The mode the output is rendered in (eg. XHTML)
367     * @param string $page the target to which should be linked
368     * @param string $filter the filter to apply to the aggregations on $page
369     * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container
370     */
371    public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) {
372        $R->internallink("$page?$filter", $this->displayValue($value));
373    }
374
375    /**
376     * This function is used to modify an aggregation query to add a filter
377     * for the given column matching the given value. A type should add at
378     * least a filter here but could do additional things like joining more
379     * tables needed to handle more complex filters
380     *
381     * Important: $value might be an array. If so, the filter should check against
382     * all provided values ORed together
383     *
384     * @param QueryBuilder $QB the query so far
385     * @param string $tablealias The table the currently saved value(s) are stored in
386     * @param string $colname The column name on above table to use in the SQL
387     * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc)
388     * @param string|string[] $value this is the user supplied value to compare against. might be multiple
389     * @param string $op the logical operator this filter should use (AND|OR)
390     */
391    public function filter(QueryBuilder $QB, $tablealias, $colname, $comp, $value, $op) {
392        /** @var QueryBuilderWhere $add Where additionional queries are added to*/
393        if(is_array($value)) {
394            $add = $QB->filters()->where($op); // sub where group
395            $op = 'OR';
396        } else {
397            $add = $QB->filters(); // main where clause
398        }
399        foreach((array) $value as $item) {
400            $pl = $QB->addValue($item);
401            $add->where($op, "$tablealias.$colname $comp $pl");
402        }
403    }
404
405    /**
406     * Add the proper selection for this type to the current Query
407     *
408     * The default implementation here should be good for nearly all types, it simply
409     * passes the given parameters to the query builder. But type may do more fancy
410     * stuff here, eg. join more tables or select multiple values and combine them to
411     * JSON. If you do, be sure implement a fitting rawValue() method.
412     *
413     * The passed $tablealias.$columnname might be a data_* table (referencing a single
414     * row) or a multi_* table (referencing multiple rows). In the latter case the
415     * multi table has already been joined with the proper conditions.
416     *
417     * You may assume a column alias named 'PID' to be available, should you need the
418     * current page context for a join or sub select.
419     *
420     * @param QueryBuilder $QB
421     * @param string $tablealias The table the currently saved value(s) are stored in
422     * @param string $colname The column name on above table
423     * @param string $alias The added selection *has* to use this column alias
424     */
425    public function select(QueryBuilder $QB, $tablealias, $colname, $alias) {
426        $QB->addSelectColumn($tablealias, $colname, $alias);
427    }
428
429    /**
430     * Sort results by this type
431     *
432     * The default implementation should be good for nearly all types. However some
433     * types may need to do proper SQLite type casting to have the right order.
434     *
435     * Generally if you implemented @see select() you probably want to implement this,
436     * too.
437     *
438     * @param QueryBuilder $QB
439     * @param string $tablealias The table the currently saved value is stored in
440     * @param string $colname The column name on above table (always single column!)
441     * @param string $order either ASC or DESC
442     */
443    public function sort(QueryBuilder $QB, $tablealias, $colname, $order) {
444        $QB->addOrderBy("$tablealias.$colname $order");
445    }
446
447    /**
448     * Get the string by which to sort values of this type
449     *
450     * This implementation is designed to work both as registered function in sqlite
451     * and to provide a string to be used in sorting values of this type in PHP.
452     *
453     * @param string|Value $string The string by which the types would usually be sorted
454     *
455     * @return string
456     */
457    public function getSortString($value) {
458        if (is_string($value)) {
459            return $value;
460        }
461        $display = $value->getDisplayValue();
462        if (is_array($display)) {
463            return blank($display[0]) ? "" : $display[0];
464        }
465        return $display;
466    }
467
468    /**
469     * This allows types to apply a transformation to the value read by select()
470     *
471     * The returned value should always be a single, non-complex string. In general
472     * it is the identifier a type stores in the database.
473     *
474     * This value will be used wherever the raw saved data is needed for comparisons.
475     * The default implementations of renderValue() and valueEditor() will call this
476     * function as well.
477     *
478     * @param string $value The value as returned by select()
479     * @return string The value as saved in the database
480     */
481    public function rawValue($value) {
482        return $value;
483    }
484
485    /**
486     * This is called when a single string is needed to represent this Type's current
487     * value as a single (non-HTML) string. Eg. in a dropdown or in autocompletion.
488     *
489     * @param string $value
490     * @return string
491     */
492    public function displayValue($value) {
493        return $this->rawValue($value);
494    }
495
496    /**
497     * Validate and optionally clean a single value
498     *
499     * This function needs to throw a validation exception when validation fails.
500     * The exception message will be prefixed by the appropriate field on output
501     *
502     * The function should return the value as it should be saved later on.
503     *
504     * @param string|int $rawvalue
505     * @return int|string the cleaned value
506     * @throws ValidationException
507     */
508    public function validate($rawvalue) {
509        return trim($rawvalue);
510    }
511
512    /**
513     * Overwrite to handle Ajax requests
514     *
515     * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will
516     * be redirected to this function on a fully initialized type. The result is
517     * JSON encoded and returned to the caller. Access additional parameter via $INPUT
518     * as usual
519     *
520     * @throws StructException when something goes wrong
521     * @return mixed
522     */
523    public function handleAjax() {
524        throw new StructException('not implemented');
525    }
526
527    /**
528     * Convenience method to access plugin language strings
529     *
530     * @param string $string
531     * @return string
532     */
533    public function getLang($string) {
534        if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct');
535        return $this->hlp->getLang($string);
536    }
537}
538