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 $name = hsc($name); 278 $rawvalue = hsc($rawvalue); 279 $id = !empty($htmlID) ? "id=\"$htmlID\"" : ''; 280 281 $html = "<input name=\"$name\" value=\"$rawvalue\" class=\"$class\" $id />"; 282 return "$html"; 283 } 284 285 /** 286 * Output the stored data 287 * 288 * @param string|int $value the value stored in the database 289 * @param \Doku_Renderer $R the renderer currently used to render the data 290 * @param string $mode The mode the output is rendered in (eg. XHTML) 291 * @return bool true if $mode could be satisfied 292 */ 293 public function renderValue($value, \Doku_Renderer $R, $mode) { 294 $value = $this->displayValue($value); 295 $R->cdata($value); 296 return true; 297 } 298 299 /** 300 * format and return the data 301 * 302 * @param int[]|string[] $values the values stored in the database 303 * @param \Doku_Renderer $R the renderer currently used to render the data 304 * @param string $mode The mode the output is rendered in (eg. XHTML) 305 * @return bool true if $mode could be satisfied 306 */ 307 public function renderMultiValue($values, \Doku_Renderer $R, $mode) { 308 $len = count($values); 309 for($i = 0; $i < $len; $i++) { 310 $this->renderValue($values[$i], $R, $mode); 311 if($i < $len - 1) { 312 $R->cdata(', '); 313 } 314 } 315 return true; 316 } 317 318 /** 319 * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary. 320 * 321 * @param string|int $value the value stored in the database 322 * @param \Doku_Renderer $R the renderer currently used to render the data 323 * @param string $mode The mode the output is rendered in (eg. XHTML) 324 * @param string $page the target to which should be linked 325 * @param string $filter the filter to apply to the aggregations on $page 326 * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container 327 */ 328 public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) { 329 $R->internallink("$page?$filter", $this->displayValue($value)); 330 } 331 332 /** 333 * This function is used to modify an aggregation query to add a filter 334 * for the given column matching the given value. A type should add at 335 * least a filter here but could do additional things like joining more 336 * tables needed to handle more complex filters 337 * 338 * Important: $value might be an array. If so, the filter should check against 339 * all provided values ORed together 340 * 341 * @param QueryBuilderWhere $add The where clause where statements can be added 342 * @param string $tablealias The table the currently saved value(s) are stored in 343 * @param string $colname The column name on above table to use in the SQL 344 * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc) 345 * @param string|string[] $value this is the user supplied value to compare against. might be multiple 346 * @param string $op the logical operator this filter should use (AND|OR) 347 */ 348 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) { 349 /** @var QueryBuilderWhere $add Where additionional queries are added to*/ 350 if(is_array($value)) { 351 $add = $add->where($op); // sub where group 352 $op = 'OR'; 353 } 354 foreach((array) $value as $item) { 355 $pl = $add->getQB()->addValue($item); 356 $add->where($op, "$tablealias.$colname $comp $pl"); 357 } 358 } 359 360 /** 361 * Add the proper selection for this type to the current Query 362 * 363 * The default implementation here should be good for nearly all types, it simply 364 * passes the given parameters to the query builder. But type may do more fancy 365 * stuff here, eg. join more tables or select multiple values and combine them to 366 * JSON. If you do, be sure implement a fitting rawValue() method. 367 * 368 * The passed $tablealias.$columnname might be a data_* table (referencing a single 369 * row) or a multi_* table (referencing multiple rows). In the latter case the 370 * multi table has already been joined with the proper conditions. 371 * 372 * You may assume a column alias named 'PID' to be available, should you need the 373 * current page context for a join or sub select. 374 * 375 * @param QueryBuilder $QB 376 * @param string $tablealias The table the currently saved value(s) are stored in 377 * @param string $colname The column name on above table 378 * @param string $alias The added selection *has* to use this column alias 379 */ 380 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) { 381 $QB->addSelectColumn($tablealias, $colname, $alias); 382 } 383 384 /** 385 * Sort results by this type 386 * 387 * The default implementation should be good for nearly all types. However some 388 * types may need to do proper SQLite type casting to have the right order. 389 * 390 * Generally if you implemented @see select() you probably want to implement this, 391 * too. 392 * 393 * @param QueryBuilder $QB 394 * @param string $tablealias The table the currently saved value is stored in 395 * @param string $colname The column name on above table (always single column!) 396 * @param string $order either ASC or DESC 397 */ 398 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) { 399 $QB->addOrderBy("$tablealias.$colname $order"); 400 } 401 402 /** 403 * Get the string by which to sort values of this type 404 * 405 * This implementation is designed to work both as registered function in sqlite 406 * and to provide a string to be used in sorting values of this type in PHP. 407 * 408 * @param string|Value $value The string by which the types would usually be sorted 409 * 410 * @return string 411 */ 412 public function getSortString($value) { 413 if (is_string($value)) { 414 return $value; 415 } 416 $display = $value->getDisplayValue(); 417 if (is_array($display)) { 418 return blank($display[0]) ? "" : $display[0]; 419 } 420 return $display; 421 } 422 423 /** 424 * This allows types to apply a transformation to the value read by select() 425 * 426 * The returned value should always be a single, non-complex string. In general 427 * it is the identifier a type stores in the database. 428 * 429 * This value will be used wherever the raw saved data is needed for comparisons. 430 * The default implementations of renderValue() and valueEditor() will call this 431 * function as well. 432 * 433 * @param string $value The value as returned by select() 434 * @return string The value as saved in the database 435 */ 436 public function rawValue($value) { 437 return $value; 438 } 439 440 /** 441 * This is called when a single string is needed to represent this Type's current 442 * value as a single (non-HTML) string. Eg. in a dropdown or in autocompletion. 443 * 444 * @param string $value 445 * @return string 446 */ 447 public function displayValue($value) { 448 return $this->rawValue($value); 449 } 450 451 /** 452 * This is the value to be used as argument to a filter for another column. 453 * 454 * In a sense this is the counterpart to the @see filter() function 455 * 456 * @param string $value 457 * 458 * @return string 459 */ 460 public function compareValue($value) { 461 return $this->rawValue($value); 462 } 463 464 /** 465 * Validate and optionally clean a single value 466 * 467 * This function needs to throw a validation exception when validation fails. 468 * The exception message will be prefixed by the appropriate field on output 469 * 470 * The function should return the value as it should be saved later on. 471 * 472 * @param string|int $rawvalue 473 * @return int|string the cleaned value 474 * @throws ValidationException 475 */ 476 public function validate($rawvalue) { 477 return trim($rawvalue); 478 } 479 480 /** 481 * Overwrite to handle Ajax requests 482 * 483 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 484 * be redirected to this function on a fully initialized type. The result is 485 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 486 * as usual 487 * 488 * @throws StructException when something goes wrong 489 * @return mixed 490 */ 491 public function handleAjax() { 492 throw new StructException('not implemented'); 493 } 494 495 /** 496 * Convenience method to access plugin language strings 497 * 498 * @param string $string 499 * @return string 500 */ 501 public function getLang($string) { 502 if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 503 return $this->hlp->getLang($string); 504 } 505 506 /** 507 * With what comparator should dynamic filters filter this type? 508 * 509 * This default does a LIKE operation 510 * 511 * @see Search::$COMPARATORS 512 * @return string 513 */ 514 public function getDefaultComparator() { 515 return '*~'; 516 } 517} 518