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