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 $class = 'struct_' . strtolower($this->getClass()); 314 315 // support the autocomplete configurations out of the box 316 if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) { 317 $class .= ' struct_autocomplete'; 318 } 319 320 $name = hsc($name); 321 $value = hsc($value); 322 $html = "<input name=\"$name\" value=\"$value\" class=\"$class\" />"; 323 return "$html"; 324 } 325 326 /** 327 * Output the stored data 328 * 329 * @param string|int $value the value stored in the database 330 * @param \Doku_Renderer $R the renderer currently used to render the data 331 * @param string $mode The mode the output is rendered in (eg. XHTML) 332 * @return bool true if $mode could be satisfied 333 */ 334 public function renderValue($value, \Doku_Renderer $R, $mode) { 335 $R->cdata($value); 336 return true; 337 } 338 339 /** 340 * format and return the data 341 * 342 * @param int[]|string[] $values the values stored in the database 343 * @param \Doku_Renderer $R the renderer currently used to render the data 344 * @param string $mode The mode the output is rendered in (eg. XHTML) 345 * @return bool true if $mode could be satisfied 346 */ 347 public function renderMultiValue($values, \Doku_Renderer $R, $mode) { 348 $len = count($values); 349 for($i = 0; $i < $len; $i++) { 350 $this->renderValue($values[$i], $R, $mode); 351 if($i < $len - 1) { 352 $R->cdata(', '); 353 } 354 } 355 return true; 356 } 357 358 /** 359 * This function builds a where clause for this column, comparing 360 * the current value stored in $column with $value. Types can use it to do 361 * clever things with the comparison. 362 * 363 * This default implementation is probably good enough for most basic types 364 * 365 * @param string $column The column name to use in the SQL 366 * @param string $comp The comparator @see Search::$COMPARATORS 367 * @param string $value 368 * @return array Tuple with the SQL and parameter array 369 */ 370 public function compare($column, $comp, $value) { 371 switch($comp) { 372 case '~': 373 $sql = "$column LIKE ?"; 374 $opt = array($value); 375 break; 376 case '!~': 377 $sql = "$column NOT LIKE ?"; 378 $opt = array($value); 379 break; 380 default: 381 $sql = "$column $comp ?"; 382 $opt = array($value); 383 } 384 385 return array($sql, $opt); 386 } 387 388 /** 389 * This function is used to modify an aggregation query to add a filter 390 * for the given column matching the given value. A type should add at 391 * least a filter here but could do additional things like joining more 392 * tables needed to handle more complex filters 393 * 394 * @param QueryBuilder $QB the query so far 395 * @param string $column The column name to use in the SQL 396 * @param string $comp The comparator @see Search::$COMPARATORS 397 * @param string $value this is the user supplied value to compare against 398 * @param string $type the logical operator this filter should use (AND|OR) 399 */ 400 public function filter(QueryBuilder $QB, $column, $comp, $value, $type) { 401 $pl = $QB->addValue($value); 402 403 switch($comp) { 404 case '~': 405 $comp = 'LIKE'; 406 break; 407 case '!~': 408 $comp = 'NOT LIKE'; 409 break; 410 } 411 412 $QB->filters()->where($type, "$column $comp $pl"); 413 } 414 415 /** 416 * Validate and optionally clean a single value 417 * 418 * This function needs to throw a validation exception when validation fails. 419 * The exception message will be prefixed by the appropriate field on output 420 * 421 * The function should return the value as it should be saved later on. 422 * 423 * @param string|int $value 424 * @return int|string the cleaned value 425 * @throws ValidationException 426 */ 427 public function validate($value) { 428 return trim($value); 429 } 430 431 /** 432 * Overwrite to handle Ajax requests 433 * 434 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 435 * be redirected to this function on a fully initialized type. The result is 436 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 437 * as usual 438 * 439 * @throws StructException when something goes wrong 440 * @return mixed 441 */ 442 public function handleAjax() { 443 throw new StructException('not implemented'); 444 } 445 446 /** 447 * Convenience method to access plugin language strings 448 * 449 * @param string $string 450 * @return string 451 */ 452 public function getLang($string) { 453 if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 454 return $this->hlp->getLang($string); 455 } 456} 457