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