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