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