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