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[] $values the current values 287 * @return string html 288 */ 289 public function multiValueEditor($name, $values) { 290 $html = ''; 291 foreach($values 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 $value the current value 311 * @param bool $isRaw set to true if the value already contains the correct raw value. e.g. for multi fields 312 * @return string html 313 */ 314 public function valueEditor($name, $value, $isRaw = false) { 315 if (!$isRaw) { 316 $value = $this->rawValue($value); 317 } 318 $class = 'struct_' . strtolower($this->getClass()); 319 320 // support the autocomplete configurations out of the box 321 if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) { 322 $class .= ' struct_autocomplete'; 323 } 324 325 $name = hsc($name); 326 $value = hsc($value); 327 $html = "<input name=\"$name\" value=\"$value\" class=\"$class\" />"; 328 return "$html"; 329 } 330 331 /** 332 * Output the stored data 333 * 334 * @param string|int $value the value stored in the database 335 * @param \Doku_Renderer $R the renderer currently used to render the data 336 * @param string $mode The mode the output is rendered in (eg. XHTML) 337 * @return bool true if $mode could be satisfied 338 */ 339 public function renderValue($value, \Doku_Renderer $R, $mode) { 340 $value = $this->rawValue($value); 341 $R->cdata($value); 342 return true; 343 } 344 345 /** 346 * format and return the data 347 * 348 * @param int[]|string[] $values the values stored in the database 349 * @param \Doku_Renderer $R the renderer currently used to render the data 350 * @param string $mode The mode the output is rendered in (eg. XHTML) 351 * @return bool true if $mode could be satisfied 352 */ 353 public function renderMultiValue($values, \Doku_Renderer $R, $mode) { 354 $len = count($values); 355 for($i = 0; $i < $len; $i++) { 356 $this->renderValue($values[$i], $R, $mode); 357 if($i < $len - 1) { 358 $R->cdata(', '); 359 } 360 } 361 return true; 362 } 363 364 /** 365 * This function is used to modify an aggregation query to add a filter 366 * for the given column matching the given value. A type should add at 367 * least a filter here but could do additional things like joining more 368 * tables needed to handle more complex filters 369 * 370 * Important: $value might be an array. If so, the filter should check against 371 * all provided values ORed together 372 * 373 * @param QueryBuilder $QB the query so far 374 * @param string $tablealias The table the currently saved value(s) are stored in 375 * @param string $colname The column name on above table to use in the SQL 376 * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc) 377 * @param string|string[] $value this is the user supplied value to compare against. might be multiple 378 * @param string $op the logical operator this filter should use (AND|OR) 379 */ 380 public function filter(QueryBuilder $QB, $tablealias, $colname, $comp, $value, $op) { 381 /** @var QueryBuilderWhere $add Where additionional queries are added to*/ 382 if(is_array($value)) { 383 $add = $QB->filters()->where($op); // sub where group 384 $op = 'OR'; 385 } else { 386 $add = $QB->filters(); // main where clause 387 } 388 foreach((array) $value as $item) { 389 $pl = $QB->addValue($item); 390 $add->where($op, "$tablealias.$colname $comp $pl"); 391 } 392 } 393 394 /** 395 * Add the proper selection for this type to the current Query 396 * 397 * The default implementation here should be good for nearly all types, it simply 398 * passes the given parameters to the query builder. But type may do more fancy 399 * stuff here, eg. join more tables or select multiple values and combine them to 400 * JSON. If you do, be sure implement a fitting rawValue() method. 401 * 402 * The passed $tablealias.$columnname might be a data_* table (referencing a single 403 * row) or a multi_* table (referencing multiple rows). In the latter case the 404 * multi table has already been joined with the proper conditions. 405 * 406 * You may assume a column alias named 'PID' to be available, should you need the 407 * current page context for a join or sub select. 408 * 409 * @param QueryBuilder $QB 410 * @param string $tablealias The table the currently saved value(s) are stored in 411 * @param string $colname The column name on above table 412 * @param string $alias The added selection *has* to use this column alias 413 */ 414 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) { 415 $QB->addSelectColumn($tablealias, $colname, $alias); 416 } 417 418 /** 419 * Sort results by this type 420 * 421 * The default implementation should be good for nearly all types. However some 422 * types may need to do proper SQLite type casting to have the right order. 423 * 424 * Generally if you implemented @see select() you probably want to implement this, 425 * too. 426 * 427 * @param QueryBuilder $QB 428 * @param string $tablealias The table the currently saved value is stored in 429 * @param string $colname The column name on above table (always single column!) 430 * @param string $order either ASC or DESC 431 */ 432 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) { 433 $QB->addOrderBy("$tablealias.$colname $order"); 434 } 435 436 /** 437 * This allows types to apply a transformation to the value read by select() 438 * 439 * The returned value should always be a single, non-complex string. In general 440 * it is the identifier a type stores in the database. 441 * 442 * This value will be used wherever the raw saved data is needed for comparisons. 443 * The default implementations of renderValue() and valueEditor() will call this 444 * function as well. 445 * 446 * @param string $value The value as returned by select() 447 * @return string The value as saved in the database 448 */ 449 public function rawValue($value) { 450 return $value; 451 } 452 453 /** 454 * Validate and optionally clean a single value 455 * 456 * This function needs to throw a validation exception when validation fails. 457 * The exception message will be prefixed by the appropriate field on output 458 * 459 * The function should return the value as it should be saved later on. 460 * 461 * @param string|int $value 462 * @return int|string the cleaned value 463 * @throws ValidationException 464 */ 465 public function validate($value) { 466 return trim($value); 467 } 468 469 /** 470 * Overwrite to handle Ajax requests 471 * 472 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 473 * be redirected to this function on a fully initialized type. The result is 474 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 475 * as usual 476 * 477 * @throws StructException when something goes wrong 478 * @return mixed 479 */ 480 public function handleAjax() { 481 throw new StructException('not implemented'); 482 } 483 484 /** 485 * Convenience method to access plugin language strings 486 * 487 * @param string $string 488 * @return string 489 */ 490 public function getLang($string) { 491 if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 492 return $this->hlp->getLang($string); 493 } 494} 495