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