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