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\TranslationUtilities; 9use dokuwiki\plugin\struct\meta\ValidationException; 10use dokuwiki\plugin\struct\meta\Value; 11 12/** 13 * Class AbstractBaseType 14 * 15 * This class represents a basic type that can be configured to be used in a Schema. It is the main 16 * part of a column definition as defined in meta\Column 17 * 18 * This defines also how the content of the coulmn will be entered and formatted. 19 * 20 * @package dokuwiki\plugin\struct\types 21 * @see Column 22 */ 23abstract class AbstractBaseType { 24 25 use TranslationUtilities; 26 27 /** 28 * @var array current config 29 */ 30 protected $config = array(); 31 32 /** 33 * @var array config keys that should not be cleaned despite not being in $config 34 */ 35 protected $keepconfig = array('label', 'hint', 'visibility'); 36 37 /** 38 * @var string label for the field 39 */ 40 protected $label = ''; 41 42 /** 43 * @var bool is this a multivalue field? 44 */ 45 protected $ismulti = false; 46 47 /** 48 * @var int the type ID 49 */ 50 protected $tid = 0; 51 52 /** 53 * @var null|Column the column context this type is part of 54 */ 55 protected $context = null; 56 57 /** 58 * @var \DokuWiki_Plugin 59 */ 60 protected $hlp = null; 61 62 /** 63 * AbstractBaseType constructor. 64 * @param array|null $config The configuration, might be null if nothing saved, yet 65 * @param string $label The label for this field (empty for new definitions= 66 * @param bool $ismulti Should this field accept multiple values? 67 * @param int $tid The id of this type if it has been saved, yet 68 */ 69 public function __construct($config = null, $label = '', $ismulti = false, $tid = 0) { 70 // general config options 71 $baseconfig = array( 72 'visibility' => array( 73 'inpage' => true, 74 'ineditor' => true, 75 ) 76 ); 77 78 // use previously saved configuration, ignoring all keys that are not supposed to be here 79 if(!is_null($config)) { 80 $this->mergeConfig($config, $this->config); 81 } 82 83 $this->initTransConfig(); 84 $this->config = array_merge($baseconfig, $this->config); 85 $this->label = $label; 86 $this->ismulti = (bool) $ismulti; 87 $this->tid = $tid; 88 } 89 90 /** 91 * Merge the current config with the base config of the type 92 * 93 * Ignores all keys that are not supposed to be there. Recurses into sub keys 94 * 95 * @param array $current Current configuration 96 * @param array $config Base Type configuration 97 */ 98 protected function mergeConfig($current, &$config) { 99 foreach($current as $key => $value) { 100 if(isset($config[$key]) || in_array($key, $this->keepconfig)) { 101 if(is_array($config[$key])) { 102 $this->mergeConfig($value, $config[$key]); 103 } else { 104 $config[$key] = $value; 105 } 106 } 107 } 108 } 109 110 /** 111 * Returns data as associative array 112 * 113 * @return array 114 */ 115 public function getAsEntry() { 116 return array( 117 'config' => json_encode($this->config), 118 'label' => $this->label, 119 'ismulti' => $this->ismulti, 120 'class' => $this->getClass() 121 ); 122 } 123 124 /** 125 * The class name of this type (no namespace) 126 * @return string 127 */ 128 public function getClass() { 129 $class = get_class($this); 130 return substr($class, strrpos($class, "\\") + 1); 131 } 132 133 /** 134 * Return the current configuration for this type 135 * 136 * @return array 137 */ 138 public function getConfig() { 139 return $this->config; 140 } 141 142 /** 143 * @return boolean 144 */ 145 public function isMulti() { 146 return $this->ismulti; 147 } 148 149 /** 150 * @return string 151 */ 152 public function getLabel() { 153 return $this->label; 154 } 155 156 /** 157 * Returns the translated label for this type 158 * 159 * Uses the current language as determined by $conf['lang']. Falls back to english 160 * and then to the type label 161 * 162 * @return string 163 */ 164 public function getTranslatedLabel() { 165 return $this->getTranslatedKey('label', $this->label); 166 } 167 168 /** 169 * Returns the translated hint for this type 170 * 171 * Uses the current language as determined by $conf['lang']. Falls back to english. 172 * Returns empty string if no hint is configured 173 * 174 * @return string 175 */ 176 public function getTranslatedHint() { 177 return $this->getTranslatedKey('hint', ''); 178 } 179 180 /** 181 * @return int 182 */ 183 public function getTid() { 184 return $this->tid; 185 } 186 187 /** 188 * @throws StructException 189 * @return Column 190 */ 191 public function getContext() { 192 if(is_null($this->context)) 193 throw new StructException('Empty column context requested. Type was probably initialized outside of Schema.'); 194 return $this->context; 195 } 196 197 /** 198 * @param Column $context 199 */ 200 public function setContext($context) { 201 $this->context = $context; 202 } 203 204 /** 205 * @return bool 206 */ 207 public function isVisibleInEditor() { 208 return $this->config['visibility']['ineditor']; 209 } 210 211 /** 212 * @return bool 213 */ 214 public function isVisibleInPage() { 215 return $this->config['visibility']['inpage']; 216 } 217 218 /** 219 * Split a single value into multiple values 220 * 221 * This function is called on saving data when only a single value instead of an array 222 * was submitted. 223 * 224 * Types implementing their own @see multiValueEditor() will probably want to override this 225 * 226 * @param string $value 227 * @return array 228 */ 229 public function splitValues($value) { 230 return array_map('trim', explode(',', $value)); 231 } 232 233 /** 234 * Return the editor to edit multiple values 235 * 236 * Types can override this to provide a better alternative than multiple entry fields 237 * 238 * @param string $name the form base name where this has to be stored 239 * @param string[] $rawvalues the current values 240 * @return string html 241 */ 242 public function multiValueEditor($name, $rawvalues) { 243 $html = ''; 244 foreach($rawvalues as $value) { 245 $html .= '<div class="multiwrap">'; 246 $html .= $this->valueEditor($name . '[]', $value); 247 $html .= '</div>'; 248 } 249 // empty field to add 250 $html .= '<div class="newtemplate">'; 251 $html .= '<div class="multiwrap">'; 252 $html .= $this->valueEditor($name . '[]', ''); 253 $html .= '</div>'; 254 $html .= '</div>'; 255 256 return $html; 257 } 258 259 /** 260 * Return the editor to edit a single value 261 * 262 * @param string $name the form name where this has to be stored 263 * @param string $rawvalue the current value 264 * @return string html 265 */ 266 public function valueEditor($name, $rawvalue) { 267 $class = 'struct_' . strtolower($this->getClass()); 268 269 // support the autocomplete configurations out of the box 270 if(isset($this->config['autocomplete']['maxresult']) && $this->config['autocomplete']['maxresult']) { 271 $class .= ' struct_autocomplete'; 272 } 273 274 $name = hsc($name); 275 $rawvalue = hsc($rawvalue); 276 $html = "<input name=\"$name\" value=\"$rawvalue\" class=\"$class\" />"; 277 return "$html"; 278 } 279 280 /** 281 * Output the stored data 282 * 283 * @param string|int $value the value stored in the database 284 * @param \Doku_Renderer $R the renderer currently used to render the data 285 * @param string $mode The mode the output is rendered in (eg. XHTML) 286 * @return bool true if $mode could be satisfied 287 */ 288 public function renderValue($value, \Doku_Renderer $R, $mode) { 289 $value = $this->displayValue($value); 290 $R->cdata($value); 291 return true; 292 } 293 294 /** 295 * format and return the data 296 * 297 * @param int[]|string[] $values the values stored in the database 298 * @param \Doku_Renderer $R the renderer currently used to render the data 299 * @param string $mode The mode the output is rendered in (eg. XHTML) 300 * @return bool true if $mode could be satisfied 301 */ 302 public function renderMultiValue($values, \Doku_Renderer $R, $mode) { 303 $len = count($values); 304 for($i = 0; $i < $len; $i++) { 305 $this->renderValue($values[$i], $R, $mode); 306 if($i < $len - 1) { 307 $R->cdata(', '); 308 } 309 } 310 return true; 311 } 312 313 /** 314 * Render a link in a struct cloud. This should be good for most types, but can be overwritten if necessary. 315 * 316 * @param string|int $value the value stored in the database 317 * @param \Doku_Renderer $R the renderer currently used to render the data 318 * @param string $mode The mode the output is rendered in (eg. XHTML) 319 * @param string $page the target to which should be linked 320 * @param string $filter the filter to apply to the aggregations on $page 321 * @param int $weight the scaled weight of the item. Will already be implemented as css font-size on the outside container 322 */ 323 public function renderTagCloudLink($value, \Doku_Renderer $R, $mode, $page, $filter, $weight) { 324 $R->internallink("$page?$filter", $this->displayValue($value)); 325 } 326 327 /** 328 * This function is used to modify an aggregation query to add a filter 329 * for the given column matching the given value. A type should add at 330 * least a filter here but could do additional things like joining more 331 * tables needed to handle more complex filters 332 * 333 * Important: $value might be an array. If so, the filter should check against 334 * all provided values ORed together 335 * 336 * @param QueryBuilderWhere $add The where clause where statements can be added 337 * @param string $tablealias The table the currently saved value(s) are stored in 338 * @param string $colname The column name on above table to use in the SQL 339 * @param string $comp The SQL comparator (LIKE, NOT LIKE, =, !=, etc) 340 * @param string|string[] $value this is the user supplied value to compare against. might be multiple 341 * @param string $op the logical operator this filter should use (AND|OR) 342 */ 343 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) { 344 /** @var QueryBuilderWhere $add Where additionional queries are added to*/ 345 if(is_array($value)) { 346 $add = $add->where($op); // sub where group 347 $op = 'OR'; 348 } 349 foreach((array) $value as $item) { 350 $pl = $add->getQB()->addValue($item); 351 $add->where($op, "$tablealias.$colname $comp $pl"); 352 } 353 } 354 355 /** 356 * Add the proper selection for this type to the current Query 357 * 358 * The default implementation here should be good for nearly all types, it simply 359 * passes the given parameters to the query builder. But type may do more fancy 360 * stuff here, eg. join more tables or select multiple values and combine them to 361 * JSON. If you do, be sure implement a fitting rawValue() method. 362 * 363 * The passed $tablealias.$columnname might be a data_* table (referencing a single 364 * row) or a multi_* table (referencing multiple rows). In the latter case the 365 * multi table has already been joined with the proper conditions. 366 * 367 * You may assume a column alias named 'PID' to be available, should you need the 368 * current page context for a join or sub select. 369 * 370 * @param QueryBuilder $QB 371 * @param string $tablealias The table the currently saved value(s) are stored in 372 * @param string $colname The column name on above table 373 * @param string $alias The added selection *has* to use this column alias 374 */ 375 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) { 376 $QB->addSelectColumn($tablealias, $colname, $alias); 377 } 378 379 /** 380 * Sort results by this type 381 * 382 * The default implementation should be good for nearly all types. However some 383 * types may need to do proper SQLite type casting to have the right order. 384 * 385 * Generally if you implemented @see select() you probably want to implement this, 386 * too. 387 * 388 * @param QueryBuilder $QB 389 * @param string $tablealias The table the currently saved value is stored in 390 * @param string $colname The column name on above table (always single column!) 391 * @param string $order either ASC or DESC 392 */ 393 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) { 394 $QB->addOrderBy("$tablealias.$colname $order"); 395 } 396 397 /** 398 * Get the string by which to sort values of this type 399 * 400 * This implementation is designed to work both as registered function in sqlite 401 * and to provide a string to be used in sorting values of this type in PHP. 402 * 403 * @param string|Value $value The string by which the types would usually be sorted 404 * 405 * @return string 406 */ 407 public function getSortString($value) { 408 if (is_string($value)) { 409 return $value; 410 } 411 $display = $value->getDisplayValue(); 412 if (is_array($display)) { 413 return blank($display[0]) ? "" : $display[0]; 414 } 415 return $display; 416 } 417 418 /** 419 * This allows types to apply a transformation to the value read by select() 420 * 421 * The returned value should always be a single, non-complex string. In general 422 * it is the identifier a type stores in the database. 423 * 424 * This value will be used wherever the raw saved data is needed for comparisons. 425 * The default implementations of renderValue() and valueEditor() will call this 426 * function as well. 427 * 428 * @param string $value The value as returned by select() 429 * @return string The value as saved in the database 430 */ 431 public function rawValue($value) { 432 return $value; 433 } 434 435 /** 436 * This is called when a single string is needed to represent this Type's current 437 * value as a single (non-HTML) string. Eg. in a dropdown or in autocompletion. 438 * 439 * @param string $value 440 * @return string 441 */ 442 public function displayValue($value) { 443 return $this->rawValue($value); 444 } 445 446 /** 447 * This is the value to be used as argument to a filter for another column. 448 * 449 * In a sense this is the counterpart to the @see filter() function 450 * 451 * @param string $value 452 * 453 * @return string 454 */ 455 public function compareValue($value) { 456 return $this->rawValue($value); 457 } 458 459 /** 460 * Validate and optionally clean a single value 461 * 462 * This function needs to throw a validation exception when validation fails. 463 * The exception message will be prefixed by the appropriate field on output 464 * 465 * The function should return the value as it should be saved later on. 466 * 467 * @param string|int $rawvalue 468 * @return int|string the cleaned value 469 * @throws ValidationException 470 */ 471 public function validate($rawvalue) { 472 return trim($rawvalue); 473 } 474 475 /** 476 * Overwrite to handle Ajax requests 477 * 478 * A call to DOKU_BASE/lib/exe/ajax.php?call=plugin_struct&column=schema.name will 479 * be redirected to this function on a fully initialized type. The result is 480 * JSON encoded and returned to the caller. Access additional parameter via $INPUT 481 * as usual 482 * 483 * @throws StructException when something goes wrong 484 * @return mixed 485 */ 486 public function handleAjax() { 487 throw new StructException('not implemented'); 488 } 489 490 /** 491 * Convenience method to access plugin language strings 492 * 493 * @param string $string 494 * @return string 495 */ 496 public function getLang($string) { 497 if(is_null($this->hlp)) $this->hlp = plugin_load('helper', 'struct'); 498 return $this->hlp->getLang($string); 499 } 500 501 /** 502 * With what comparator should dynamic filters filter this type? 503 * 504 * This default does a LIKE operation 505 * 506 * @see Search::$COMPARATORS 507 * @return string 508 */ 509 public function getDefaultComparator() { 510 return '*~'; 511 } 512} 513