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