1<?php 2namespace dokuwiki\plugin\struct\types; 3 4use dokuwiki\plugin\struct\meta\Column; 5use dokuwiki\plugin\struct\meta\StructException; 6use dokuwiki\plugin\struct\meta\ValidationException; 7 8/** 9 * Class AbstractBaseType 10 * 11 * This class represents a basic type that can be configured to be used in a Schema. It is the main 12 * part of a column definition as defined in meta\Column 13 * 14 * This defines also how the content of the coulmn will be entered and formatted. 15 * 16 * @package dokuwiki\plugin\struct\types 17 * @see Column 18 */ 19abstract class AbstractBaseType { 20 21 /** 22 * @var array current config 23 */ 24 protected $config = array(); 25 26 /** 27 * @var array config keys that should not be cleaned despite not being in $config 28 */ 29 protected $keepconfig = array('label', 'hint', 'visibility'); 30 31 /** 32 * @var string label for the field 33 */ 34 protected $label = ''; 35 36 /** 37 * @var bool is this a multivalue field? 38 */ 39 protected $ismulti = false; 40 41 /** 42 * @var int the type ID 43 */ 44 protected $tid = 0; 45 46 /** 47 * @var null|Column the column context this type is part of 48 */ 49 protected $context = null; 50 51 /** 52 * @var \DokuWiki_Plugin 53 */ 54 protected $hlp = null; 55 56 /** 57 * AbstractBaseType constructor. 58 * @param array|null $config The configuration, might be null if nothing saved, yet 59 * @param string $label The label for this field (empty for new definitions= 60 * @param bool $ismulti Should this field accept multiple values? 61 * @param int $tid The id of this type if it has been saved, yet 62 */ 63 public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) { 64 // general config options 65 $baseconfig = array( 66 'visibility' => array( 67 'inpage' => true, 68 'ineditor' => true, 69 ) 70 ); 71 72 // use previously saved configuration, ignoring all keys that are not supposed to be here 73 if(!is_null($config)) { 74 $this->mergeConfig($config, $this->config); 75 } 76 77 $this->initTransConfig(); 78 $this->config = array_merge($baseconfig, $this->config); 79 $this->label = $label; 80 $this->ismulti = (bool) $ismulti; 81 $this->tid = $tid; 82 } 83 84 /** 85 * Merge the current config with the base config of the type 86 * 87 * Ignores all keys that are not supposed to be there. Recurses into sub keys 88 * 89 * @param array $current Current configuration 90 * @param array $config Base Type configuration 91 */ 92 protected function mergeConfig($current, &$config) { 93 foreach($current as $key => $value) { 94 if(isset($config[$key]) || in_array($key, $this->keepconfig)) { 95 if(is_array($config[$key])) { 96 $this->mergeConfig($value, $config[$key]); 97 } else { 98 $config[$key] = $value; 99 } 100 } 101 } 102 } 103 104 /** 105 * Add the translatable keys to the configuration 106 * 107 * This checks if a configuration for the translation plugin exists and if so 108 * adds all configured languages to the config array. This ensures all types 109 * can have translatable labels. 110 */ 111 protected function initTransConfig() { 112 global $conf; 113 $lang = $conf['lang']; 114 if(isset($conf['plugin']['translation']['translations'])) { 115 $lang .= ' ' . $conf['plugin']['translation']['translations']; 116 } 117 $langs = explode(' ', $lang); 118 $langs = array_map('trim', $langs); 119 $langs = array_filter($langs); 120 $langs = array_unique($langs); 121 122 if(!isset($this->config['label'])) $this->config['label'] = array(); 123 if(!isset($this->config['hint'])) $this->config['hint'] = array(); 124 // initialize missing keys 125 foreach($langs as $lang) { 126 if(!isset($this->config['label'][$lang])) $this->config['label'][$lang] = ''; 127 if(!isset($this->config['hint'][$lang])) $this->config['hint'][$lang] = ''; 128 } 129 // strip unknown languages 130 foreach(array_keys($this->config['label']) as $key) { 131 if(!in_array($key, $langs)) unset($this->config['label'][$key]); 132 } 133 foreach(array_keys($this->config['hint']) as $key) { 134 if(!in_array($key, $langs)) unset($this->config['hint'][$key]); 135 } 136 137 } 138 139 /** 140 * Returns data as associative array 141 * 142 * @return array 143 */ 144 public function getAsEntry() { 145 return array( 146 'config' => json_encode($this->config), 147 'label' => $this->label, 148 'ismulti' => $this->ismulti, 149 'class' => $this->getClass() 150 ); 151 } 152 153 /** 154 * The class name of this type (no namespace) 155 * @return string 156 */ 157 public function getClass() { 158 $class = get_class($this); 159 return substr($class, strrpos($class, "\\") + 1); 160 } 161 162 /** 163 * Return the current configuration for this type 164 * 165 * @return array 166 */ 167 public function getConfig() { 168 return $this->config; 169 } 170 171 /** 172 * @return boolean 173 */ 174 public function isMulti() { 175 return $this->ismulti; 176 } 177 178 /** 179 * @return string 180 */ 181 public function getLabel() { 182 return $this->label; 183 } 184 185 /** 186 * Returns the translated label for this type 187 * 188 * Uses the current language as determined by $conf['lang']. Falls back to english 189 * and then to the Schema label 190 * 191 * @return string 192 */ 193 public function getTranslatedLabel() { 194 global $conf; 195 $lang = $conf['lang']; 196 if(!blank($this->config['label'][$lang])) { 197 return $this->config['label'][$lang]; 198 } 199 if(!blank($this->config['label']['en'])) { 200 return $this->config['label']['en']; 201 } 202 return $this->label; 203 } 204 205 /** 206 * Returns the translated hint for this type 207 * 208 * Uses the current language as determined by $conf['lang']. Falls back to english. 209 * Returns empty string if no hint is configured 210 * 211 * @return string 212 */ 213 public function getTranslatedHint() { 214 global $conf; 215 $lang = $conf['lang']; 216 if(!blank($this->config['hint'][$lang])) { 217 return $this->config['hint'][$lang]; 218 } 219 if(!blank($this->config['hint']['en'])) { 220 return $this->config['hint']['en']; 221 } 222 return ''; 223 } 224 225 /** 226 * @return int 227 */ 228 public function getTid() { 229 return $this->tid; 230 } 231 232 /** 233 * @throws StructException 234 * @return Column 235 */ 236 public function getContext() { 237 if(is_null($this->context)) 238 throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.'); 239 return $this->context; 240 } 241 242 /** 243 * @param Column $context 244 */ 245 public function setContext($context) { 246 $this->context = $context; 247 } 248 249 /** 250 * @return bool 251 */ 252 public function isVisibleInEditor() { 253 return $this->config['visibility']['ineditor']; 254 } 255 256 /** 257 * @return bool 258 */ 259 public function isVisibleInPage() { 260 return $this->config['visibility']['inpage']; 261 } 262 263 /** 264 * Split a single value into multiple values 265 * 266 * This function is called on saving data when only a single value instead of an array 267 * was submitted. 268 * 269 * Types implementing their own @see multiValueEditor() will probably want to override this 270 * 271 * @param string $value 272 * @return array 273 */ 274 public function splitValues($value) { 275 return array_map('trim', explode(',', $value)); 276 } 277 278 /** 279 * Return the editor to edit multiple values 280 * 281 * Types can override this to provide a better alternative than multiple entry fields 282 * 283 * @param string $name the form base name where this has to be stored 284 * @param string[] $values the current values 285 * @return string html 286 */ 287 public function multiValueEditor($name, $values) { 288 $html = ''; 289 foreach($values as $value) { 290 $html .= '<div class="multiwrap">'; 291 $html .= $this->valueEditor($name . '[]', $value); 292 $html .= '</div>'; 293 } 294 // empty field to add 295 $html .= '<div class="newtemplate">'; 296 $html .= '<div class="multiwrap">'; 297 $html .= $this->valueEditor($name . '[]', ''); 298 $html .= '</div>'; 299 $html .= '</div>'; 300 301 return $html; 302 } 303 304 /** 305 * Return the editor to edit a single value 306 * 307 * @param string $name the form name where this has to be stored 308 * @param string $value the current value 309 * @return string html 310 */ 311 public function valueEditor($name, $value) { 312 $class = 'struct_' . strtolower($this->getClass()); 313 314 // support the autocomplete configurations out of the box 315 if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) { 316 $class .= ' struct_autocomplete'; 317 } 318 319 $name = hsc($name); 320 $value = hsc($value); 321 $html = "<input name=\"$name\" value=\"$value\" class=\"$class\" />"; 322 return "$html"; 323 } 324 325 /** 326 * Output the stored data 327 * 328 * @param string|int $value the value stored in the database 329 * @param \Doku_Renderer $R the renderer currently used to render the data 330 * @param string $mode The mode the output is rendered in (eg. XHTML) 331 * @return bool true if $mode could be satisfied 332 */ 333 public function renderValue($value, \Doku_Renderer $R, $mode) { 334 $R->cdata($value); 335 return true; 336 } 337 338 /** 339 * format and return the data 340 * 341 * @param int[]|string[] $values the values stored in the database 342 * @param \Doku_Renderer $R the renderer currently used to render the data 343 * @param string $mode The mode the output is rendered in (eg. XHTML) 344 * @return bool true if $mode could be satisfied 345 */ 346 public function renderMultiValue($values, \Doku_Renderer $R, $mode) { 347 $len = count($values); 348 for($i = 0; $i < $len; $i++) { 349 $this->renderValue($values[$i], $R, $mode); 350 if($i < $len - 1) { 351 $R->cdata(', '); 352 } 353 } 354 return true; 355 } 356 357 /** 358 * This function builds a where clause for this column, comparing 359 * the current value stored in $column with $value. Types can use it to do 360 * clever things with the comparison. 361 * 362 * This default implementation is probably good enough for most basic types 363 * 364 * @param string $column The column name to us in the SQL 365 * @param string $comp The comparator @see Search::$COMPARATORS 366 * @param string $value 367 * @return array Tuple with the SQL and parameter array 368 */ 369 public function compare($column, $comp, $value) { 370 switch($comp) { 371 case '~': 372 $sql = "$column LIKE ?"; 373 $opt = array($value); 374 break; 375 case '!~': 376 $sql = "$column NOT LIKE ?"; 377 $opt = array($value); 378 break; 379 default: 380 $sql = "$column $comp ?"; 381 $opt = array($value); 382 } 383 384 return array($sql, $opt); 385 } 386 387 /** 388 * Validate and optionally clean a single value 389 * 390 * This function needs to throw a validation exception when validation fails. 391 * The exception message will be prefixed by the appropriate field on output 392 * 393 * The function should return the value as it should be saved later on. 394 * 395 * @param string|int $value 396 * @return int|string the cleaned value 397 * @throws ValidationException 398 */ 399 public function validate($value) { 400 return trim($value); 401 } 402 403 /** 404 * Overwrite to handle Ajax requests 405 * 406 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 407 * be redirected to this function on a fully initialized type. The result is 408 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 409 * as usual 410 * 411 * @throws StructException when something goes wrong 412 * @return mixed 413 */ 414 public function handleAjax() { 415 throw new StructException('not implemented'); 416 } 417 418 /** 419 * Convenience method to access plugin language strings 420 * 421 * @param string $string 422 * @return string 423 */ 424 public function getLang($string) { 425 if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 426 return $this->hlp->getLang($string); 427 } 428} 429