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