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('Empty column context requested. Type was probably initialized outside of Schema.'); 206 return $this->context; 207 } 208 209 /** 210 * @param Column $context 211 */ 212 public function setContext($context) 213 { 214 $this->context = $context; 215 } 216 217 /** 218 * @return bool 219 */ 220 public function isVisibleInEditor() 221 { 222 return $this->config['visibility']['ineditor']; 223 } 224 225 /** 226 * @return bool 227 */ 228 public function isVisibleInPage() 229 { 230 return $this->config['visibility']['inpage']; 231 } 232 233 /** 234 * Split a single value into multiple values 235 * 236 * This function is called on saving data when only a single value instead of an array 237 * was submitted. 238 * 239 * Types implementing their own @param string $value 240 * @return array 241 * @see multiValueEditor() will probably want to override this 242 * 243 */ 244 public function splitValues($value) 245 { 246 return array_map('trim', explode(',', $value)); 247 } 248 249 /** 250 * Return the editor to edit multiple values 251 * 252 * Types can override this to provide a better alternative than multiple entry fields 253 * 254 * @param string $name the form base name where this has to be stored 255 * @param string[] $rawvalues the current values 256 * @param string $htmlID a unique id to be referenced by the label 257 * @return string html 258 */ 259 public function multiValueEditor($name, $rawvalues, $htmlID) 260 { 261 $html = ''; 262 foreach ($rawvalues as $value) { 263 $html .= '<div class="multiwrap">'; 264 $html .= $this->valueEditor($name . '[]', $value, ''); 265 $html .= '</div>'; 266 } 267 // empty field to add 268 $html .= '<div class="newtemplate">'; 269 $html .= '<div class="multiwrap">'; 270 $html .= $this->valueEditor($name . '[]', '', $htmlID); 271 $html .= '</div>'; 272 $html .= '</div>'; 273 274 return $html; 275 } 276 277 /** 278 * Return the editor to edit a single value 279 * 280 * @param string $name the form name where this has to be stored 281 * @param string $rawvalue the current value 282 * @param string $htmlID a unique id to be referenced by the label 283 * 284 * @return string html 285 */ 286 public function valueEditor($name, $rawvalue, $htmlID) 287 { 288 $class = 'struct_' . strtolower($this->getClass()); 289 290 // support the autocomplete configurations out of the box 291 if (isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) { 292 $class .= ' struct_autocomplete'; 293 } 294 295 $params = array( 296 'name' => $name, 297 'value' => $rawvalue, 298 'class' => $class, 299 'id' => $htmlID 300 ); 301 $attributes = buildAttributes($params, true); 302 return "<input $attributes>"; 303 } 304 305 /** 306 * Output the stored data 307 * 308 * @param string|int $value the value stored in the database 309 * @param \Doku_Renderer $R the renderer currently used to render the data 310 * @param string $mode The mode the output is rendered in (eg. XHTML) 311 * @return bool true if $mode could be satisfied 312 */ 313 public function renderValue($value, \Doku_Renderer $R, $mode) 314 { 315 $value = $this->displayValue($value); 316 $R->cdata($value); 317 return true; 318 } 319 320 /** 321 * format and return the data 322 * 323 * @param int[]|string[] $values the values stored in the database 324 * @param \Doku_Renderer $R the renderer currently used to render the data 325 * @param string $mode The mode the output is rendered in (eg. XHTML) 326 * @return bool true if $mode could be satisfied 327 */ 328 public function renderMultiValue($values, \Doku_Renderer $R, $mode) 329 { 330 $len = count($values); 331 for ($i = 0; $i < $len; $i++) { 332 $this->renderValue($values[$i], $R, $mode); 333 if ($i < $len - 1) { 334 $R->cdata(', '); 335 } 336 } 337 return true; 338 } 339 340 /** 341 * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary. 342 * 343 * @param string|int $value the value stored in the database 344 * @param \Doku_Renderer $R the renderer currently used to render the data 345 * @param string $mode The mode the output is rendered in (eg. XHTML) 346 * @param string $page the target to which should be linked 347 * @param string $filter the filter to apply to the aggregations on $page 348 * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container 349 */ 350 public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) 351 { 352 $R->internallink("$page?$filter", $this->displayValue($value)); 353 } 354 355 /** 356 * This function is used to modify an aggregation query to add a filter 357 * for the given column matching the given value. A type should add at 358 * least a filter here but could do additional things like joining more 359 * tables needed to handle more complex filters 360 * 361 * Important: $value might be an array. If so, the filter should check against 362 * all provided values ORed together 363 * 364 * @param QueryBuilderWhere $add The where clause where statements can be added 365 * @param string $tablealias The table the currently saved value(s) are stored in 366 * @param string $colname The column name on above table to use in the SQL 367 * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc) 368 * @param string|string[] $value this is the user supplied value to compare against. might be multiple 369 * @param string $op the logical operator this filter should use (AND|OR) 370 */ 371 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 372 { 373 /** @var QueryBuilderWhere $add Where additionional queries are added to */ 374 if (is_array($value)) { 375 $add = $add->where($op); // sub where group 376 $op = 'OR'; 377 } 378 foreach ((array)$value as $item) { 379 $pl = $add->getQB()->addValue($item); 380 $add->where($op, "$tablealias.$colname $comp $pl"); 381 } 382 } 383 384 /** 385 * Add the proper selection for this type to the current Query 386 * 387 * The default implementation here should be good for nearly all types, it simply 388 * passes the given parameters to the query builder. But type may do more fancy 389 * stuff here, eg. join more tables or select multiple values and combine them to 390 * JSON. If you do, be sure implement a fitting rawValue() method. 391 * 392 * The passed $tablealias.$columnname might be a data_* table (referencing a single 393 * row) or a multi_* table (referencing multiple rows). In the latter case the 394 * multi table has already been joined with the proper conditions. 395 * 396 * You may assume a column alias named 'PID' to be available, should you need the 397 * current page context for a join or sub select. 398 * 399 * @param QueryBuilder $QB 400 * @param string $tablealias The table the currently saved value(s) are stored in 401 * @param string $colname The column name on above table 402 * @param string $alias The added selection *has* to use this column alias 403 */ 404 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) 405 { 406 $QB->addSelectColumn($tablealias, $colname, $alias); 407 } 408 409 /** 410 * Sort results by this type 411 * 412 * The default implementation should be good for nearly all types. However some 413 * types may need to do proper SQLite type casting to have the right order. 414 * 415 * Generally if you implemented @param QueryBuilder $QB 416 * @param string $tablealias The table the currently saved value is stored in 417 * @param string $colname The column name on above table (always single column!) 418 * @param string $order either ASC or DESC 419 * @see select() you probably want to implement this, 420 * too. 421 * 422 */ 423 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 424 { 425 $QB->addOrderBy("$tablealias.$colname $order"); 426 } 427 428 /** 429 * Get the string by which to sort values of this type 430 * 431 * This implementation is designed to work both as registered function in sqlite 432 * and to provide a string to be used in sorting values of this type in PHP. 433 * 434 * @param string|Value $value The string by which the types would usually be sorted 435 * 436 * @return string 437 */ 438 public function getSortString($value) 439 { 440 if (is_string($value)) { 441 return $value; 442 } 443 $display = $value->getDisplayValue(); 444 if (is_array($display)) { 445 return blank($display[0]) ? "" : $display[0]; 446 } 447 return $display; 448 } 449 450 /** 451 * This allows types to apply a transformation to the value read by select() 452 * 453 * The returned value should always be a single, non-complex string. In general 454 * it is the identifier a type stores in the database. 455 * 456 * This value will be used wherever the raw saved data is needed for comparisons. 457 * The default implementations of renderValue() and valueEditor() will call this 458 * function as well. 459 * 460 * @param string $value The value as returned by select() 461 * @return string The value as saved in the database 462 */ 463 public function rawValue($value) 464 { 465 return $value; 466 } 467 468 /** 469 * This is called when a single string is needed to represent this Type's current 470 * value as a single (non-HTML) string. Eg. in a dropdown or in autocompletion. 471 * 472 * @param string $value 473 * @return string 474 */ 475 public function displayValue($value) 476 { 477 return $this->rawValue($value); 478 } 479 480 /** 481 * This is the value to be used as argument to a filter for another column. 482 * 483 * In a sense this is the counterpart to the @param string $value 484 * 485 * @return string 486 * @see filter() function 487 * 488 */ 489 public function compareValue($value) 490 { 491 return $this->rawValue($value); 492 } 493 494 /** 495 * Validate and optionally clean a single value 496 * 497 * This function needs to throw a validation exception when validation fails. 498 * The exception message will be prefixed by the appropriate field on output 499 * 500 * The function should return the value as it should be saved later on. 501 * 502 * @param string|int $rawvalue 503 * @return int|string the cleaned value 504 * @throws ValidationException 505 */ 506 public function validate($rawvalue) 507 { 508 return trim($rawvalue); 509 } 510 511 /** 512 * Overwrite to handle Ajax requests 513 * 514 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 515 * be redirected to this function on a fully initialized type. The result is 516 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 517 * as usual 518 * 519 * @return mixed 520 * @throws StructException when something goes wrong 521 */ 522 public function handleAjax() 523 { 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 { 535 if (is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 536 return $this->hlp->getLang($string); 537 } 538 539 /** 540 * With what comparator should dynamic filters filter this type? 541 * 542 * This default does a LIKE operation 543 * 544 * @return string 545 * @see Search::$COMPARATORS 546 */ 547 public function getDefaultComparator() 548 { 549 return '*~'; 550 } 551} 552