1<?php 2 3/** 4 * Base class for form fields 5 * 6 * This class provides basic functionality for many form fields. It supports 7 * labels, basic validation and template-based XHTML output. 8 * 9 * @author Adrian Lang <lang@cosmocode.de> 10 **/ 11 12/** 13 * Class helper_plugin_bureaucracy_field 14 * 15 * base class for all the form fields 16 */ 17class helper_plugin_bureaucracy_field extends syntax_plugin_bureaucracy { 18 19 protected $mandatory_args = 2; 20 public $opt = array(); 21 /** @var string|array */ 22 protected $tpl; 23 protected $checks = array(); 24 public $hidden = false; 25 protected $error = false; 26 protected $checktypes = array( 27 '/' => 'match', 28 '<' => 'max', 29 '>' => 'min' 30 ); 31 32 /** 33 * Construct a helper_plugin_bureaucracy_field object 34 * 35 * This constructor initializes a helper_plugin_bureaucracy_field object 36 * based on a given definition. 37 * 38 * The first two items represent: 39 * * the type of the field 40 * * and the label the field has been given. 41 * Additional arguments are type-specific mandatory extra arguments and optional arguments. 42 * 43 * The optional arguments may add constraints to the field value, provide a 44 * default value, mark the field as optional or define that the field is 45 * part of a pagename (when using the template action). 46 * 47 * Since the field objects are cached, this constructor may not reference 48 * request data. 49 * 50 * @param array $args The tokenized definition, only split at spaces 51 */ 52 public function initialize($args) { 53 $this->init($args); 54 $this->standardArgs($args); 55 } 56 57 /** 58 * Return false to prevent DokuWiki reusing instances of the plugin 59 * 60 * @return bool 61 */ 62 public function isSingleton() { 63 return false; 64 } 65 66 /** 67 * Checks number of arguments and store 'cmd', 'label' and 'display' values 68 * 69 * @param array $args array with the definition 70 */ 71 protected function init(&$args) { 72 if(count($args) < $this->mandatory_args){ 73 msg(sprintf($this->getLang('e_missingargs'), hsc($args[0]), 74 hsc($args[1])), -1); 75 return; 76 } 77 78 // get standard arguments 79 $this->opt = array(); 80 foreach (array('cmd', 'label') as $key) { 81 if (count($args) === 0) break; 82 $this->opt[$key] = array_shift($args); 83 } 84 $this->opt['display'] = $this->opt['label']; // allow to modify display value independently 85 } 86 87 /** 88 * Check for additional arguments and store their values 89 * 90 * @param array $args array with remaining definition arguments 91 */ 92 protected function standardArgs($args) { 93 // parse additional arguments 94 foreach($args as $arg){ 95 if ($arg[0] == '=') { 96 $this->setVal(substr($arg,1)); 97 } elseif ($arg == '!') { 98 $this->opt['optional'] = true; 99 } elseif ($arg == '^') { 100 //only one field has focus 101 if (helper_plugin_bureaucracy_field::hasFocus()) { 102 $this->opt['id'] = 'focus__this'; 103 } 104 } elseif($arg == '@') { 105 $this->opt['pagename'] = true; 106 } elseif($arg == '@@') { 107 $this->opt['replyto'] = true; 108 } elseif(preg_match('/x\d/', $arg)) { 109 $this->opt['rows'] = substr($arg,1); 110 } elseif($arg[0] == '.') { 111 $this->opt['class'] = substr($arg, 1); 112 } elseif(preg_match('/^0{2,}$/', $arg)) { 113 $this->opt['leadingzeros'] = strlen($arg); 114 } elseif($arg[0].$arg[1] == '**') { 115 $this->opt['matchexplanation'] = substr($arg,2); 116 } else { 117 $t = $arg[0]; 118 $d = substr($arg,1); 119 if (in_array($t, array('>', '<')) && !is_numeric($d)) { 120 break; 121 } 122 if ($t == '/') { 123 if (substr($d, -1) !== '/') { 124 break; 125 } 126 $d = substr($d, 0, -1); 127 } 128 if (!isset($this->checktypes[$t]) || !method_exists($this, 'validate_' . $this->checktypes[$t])) { 129 msg(sprintf($this->getLang('e_unknownconstraint'), hsc($t).' ('.hsc($arg).')'), -1); 130 return; 131 } 132 $this->checks[] = array('t' => $t, 'd' => $d); 133 } 134 } 135 } 136 137 /** 138 * Add parsed element to Form which generates XHTML 139 * 140 * Outputs the represented field using the passed Doku_Form object. 141 * Additional parameters (CSS class & HTML name) are passed in $params. 142 * HTML output is created by passing the template $this->tpl to the simple 143 * template engine _parse_tpl. 144 * 145 * @param array $params Additional HTML specific parameters 146 * @param Doku_Form $form The target Doku_Form object 147 * @param int $formid unique identifier of the form which contains this field 148 */ 149 public function renderfield($params, Doku_Form $form, $formid) { 150 $this->_handlePreload(); 151 if(!$form->_infieldset){ 152 $form->startFieldset(''); 153 } 154 if ($this->error) { 155 $params['class'] = 'bureaucracy_error'; 156 } 157 158 $params = array_merge($this->opt, $params); 159 $form->addElement($this->_parse_tpl($this->tpl, $params)); 160 } 161 162 /** 163 * Only the first use get the focus, next calls not 164 * 165 * @return bool 166 */ 167 protected static function hasFocus(){ 168 static $focus = true; 169 if($focus) { 170 $focus = false; 171 return true; 172 } else { 173 return false; 174 } 175 } 176 177 178 /** 179 * Check for preload value in the request url 180 */ 181 protected function _handlePreload() { 182 $preload_name = '@' . strtr($this->getParam('label'),' .','__') . '@'; 183 if (isset($_GET[$preload_name])) { 184 $this->setVal($_GET[$preload_name]); 185 } 186 } 187 188 /** 189 * Handle a post to the field 190 * 191 * Accepts and validates a posted value. 192 * 193 * (Overridden by fieldset, which has as argument an array with the form array by reference) 194 * 195 * @param string $value The passed value or array or null if none given 196 * @param helper_plugin_bureaucracy_field[] $fields (reference) form fields (POST handled upto $this field) 197 * @param int $index index number of field in form 198 * @param int $formid unique identifier of the form which contains this field 199 * @return bool Whether the passed value is valid 200 */ 201 public function handle_post($value, &$fields, $index, $formid) { 202 return $this->hidden || $this->setVal($value); 203 } 204 205 /** 206 * Get the field type 207 * 208 * @return string 209 **/ 210 public function getFieldType() { 211 return $this->opt['cmd']; 212 } 213 214 /** 215 * Get the replacement pattern used by action 216 * 217 * @return string 218 */ 219 public function getReplacementPattern() { 220 $label = $this->getParam('label'); 221 $value = $this->getParam('value'); 222 223 if (is_array($value)) { 224 return '/(@@|##)' . preg_quote($label, '/') . 225 '(?:\((?P<delimiter>.*?)\))?' .//delimiter 226 '(?:\|(?P<default>.*?))' . (count($value) == 0 ? '' : '?') . 227 '\1/si'; 228 } 229 230 return '/(@@|##)' . preg_quote($label, '/') . 231 '(?:\|(.*?))' . (is_null($value) ? '' : '?') . 232 '\1/si'; 233 } 234 235 /** 236 * Used as an callback for preg_replace_callback 237 * 238 * @param $matches 239 * @return string 240 */ 241 public function replacementMultiValueCallback($matches) { 242 $value = $this->opt['value']; 243 244 //default value 245 if (is_null($value) || $value === false) { 246 if (isset($matches['default']) && $matches['default'] != '') { 247 return $matches['default']; 248 } 249 return $matches[0]; 250 } 251 252 //check if matched string containts a pair of brackets 253 $delimiter = preg_match('/\(.*\)/s', $matches[0]) ? $matches['delimiter'] : ', '; 254 255 return implode($delimiter, $value); 256 } 257 258 /** 259 * Get the value used by action 260 * If value is a callback preg_replace_callback is called instead preg_replace 261 * 262 * @return mixed|string 263 */ 264 public function getReplacementValue() { 265 $value = $this->getParam('value'); 266 267 if (is_array($value)) { 268 return array($this, 'replacementMultiValueCallback'); 269 } 270 271 return is_null($value) || $value === false ? '$2' : $value; 272 } 273 274 /** 275 * Validate value and stores it 276 * 277 * @param mixed $value value entered into field 278 * @return bool whether the passed value is valid 279 */ 280 protected function setVal($value) { 281 if ($value === '') { 282 $value = null; 283 } 284 $this->opt['value'] = $value; 285 try { 286 $this->_validate(); 287 $this->error = false; 288 } catch (Exception $e) { 289 msg($e->getMessage(), -1); 290 $this->error = true; 291 } 292 return !$this->error; 293 } 294 295 /** 296 * Whether the field is true (used for depending fieldsets) 297 * 298 * @return bool whether field is set 299 */ 300 public function isSet_() { 301 return !is_null($this->getParam('value')); 302 } 303 304 /** 305 * Validate value of field and throws exceptions for bad values. 306 * 307 * @throws Exception when field didn't validate. 308 */ 309 protected function _validate() { 310 $value = $this->getParam('value'); 311 if (is_null($value)) { 312 if(!isset($this->opt['optional'])) { 313 throw new Exception(sprintf($this->getLang('e_required'),hsc($this->opt['label']))); 314 } 315 return; 316 } 317 318 foreach ($this->checks as $check) { 319 $checktype = $this->checktypes[$check['t']]; 320 if (!call_user_func(array($this, 'validate_' . $checktype), $check['d'], $value)) { 321 //replacement is custom explanation or just the regexp or the requested value 322 if(isset($this->opt['matchexplanation'])) { 323 $replacement = hsc($this->opt['matchexplanation']); 324 } elseif($checktype == 'match') { 325 $replacement = sprintf($this->getLang('checkagainst'), hsc($check['d'])); 326 } else { 327 $replacement = hsc($check['d']); 328 } 329 330 throw new Exception(sprintf($this->getLang('e_' . $checktype), hsc($this->opt['label']), $replacement)); 331 } 332 } 333 } 334 335 /** 336 * Get an arbitrary parameter 337 * 338 * @param string $name 339 * @return mixed|null 340 */ 341 public function getParam($name) { 342 if (!isset($this->opt[$name]) || $name === 'value' && $this->hidden) { 343 return null; 344 } 345 if ($name === 'pagename') { 346 // If $this->opt['pagename'] is set, return the escaped value of the field. 347 $value = $this->getParam('value'); 348 if (is_null($value)) { 349 return null; 350 } 351 global $conf; 352 if($conf['useslash']) $value = str_replace('/',' ',$value); 353 return str_replace(':',' ',$value); 354 } 355 return $this->opt[$name]; 356 } 357 358 /** 359 * Parse a template with given parameters 360 * 361 * Replaces variables specified like @@VARNAME|default@@ using the passed 362 * value map. 363 * 364 * @param string|array $tpl The template as string or array 365 * @param array $params A hash mapping parameters to values 366 * 367 * @return string|array The parsed template 368 */ 369 protected function _parse_tpl($tpl, $params) { 370 // addElement supports a special array format as well. In this case 371 // not all elements should be escaped. 372 $is_simple = !is_array($tpl); 373 if ($is_simple) $tpl = array($tpl); 374 375 foreach ($tpl as &$val) { 376 // Select box passes options as an array. We do not escape those. 377 if (is_array($val)) continue; 378 379 // find all variables and their defaults or param values 380 preg_match_all('/@@([A-Z]+)(?:\|((?:[^@]|@$|@[^@])*))?@@/', $val, $pregs); 381 for ($i = 0 ; $i < count($pregs[2]) ; ++$i) { 382 if (isset($params[strtolower($pregs[1][$i])])) { 383 $pregs[2][$i] = $params[strtolower($pregs[1][$i])]; 384 } 385 } 386 // we now have placeholders in $pregs[0] and their values in $pregs[2] 387 $replacements = array(); // check if empty to prevent php 5.3 warning 388 if (!empty($pregs[0])) { 389 $replacements = array_combine($pregs[0], $pregs[2]); 390 } 391 392 if($is_simple){ 393 // for simple string templates, we escape all replacements 394 $replacements = array_map('hsc', $replacements); 395 }else{ 396 // for the array ones, we escape the label and display only 397 if(isset($replacements['@@LABEL@@'])) $replacements['@@LABEL@@'] = hsc($replacements['@@LABEL@@']); 398 if(isset($replacements['@@DISPLAY@@'])) $replacements['@@DISPLAY@@'] = hsc($replacements['@@DISPLAY@@']); 399 } 400 401 // we attach a mandatory marker to the display 402 if(isset($replacements['@@DISPLAY@@']) && !isset($params['optional'])){ 403 $replacements['@@DISPLAY@@'] .= ' <sup>*</sup>'; 404 } 405 $val = str_replace(array_keys($replacements), array_values($replacements), $val); 406 } 407 return $is_simple ? $tpl[0] : $tpl; 408 } 409 410 /** 411 * Executed after performing the action hooks 412 */ 413 public function after_action() { 414 } 415 416 /** 417 * Constraint function: value of field should match this regexp 418 * 419 * @param string $d regexp 420 * @param mixed $value 421 * @return int|bool 422 */ 423 protected function validate_match($d, $value) { 424 return @preg_match('/' . $d . '/i', $value); 425 } 426 427 /** 428 * Constraint function: value of field should be bigger 429 * 430 * @param int|number $d lower bound 431 * @param mixed $value of field 432 * @return bool 433 */ 434 protected function validate_min($d, $value) { 435 return $value > $d; 436 } 437 438 /** 439 * Constraint function: value of field should be smaller 440 * 441 * @param int|number $d upper bound 442 * @param mixed $value of field 443 * @return bool 444 */ 445 protected function validate_max($d, $value) { 446 return $value < $d; 447 } 448 449 /** 450 * Available methods 451 * 452 * @return array 453 */ 454 public function getMethods() { 455 $result = array(); 456 $result[] = array( 457 'name' => 'initialize', 458 'desc' => 'Initiate object, first parameters are at least cmd and label', 459 'params' => array( 460 'params' => 'array' 461 ) 462 ); 463 $result[] = array( 464 'name' => 'renderfield', 465 'desc' => 'Add parsed element to Form which generates XHTML', 466 'params' => array( 467 'params' => 'array', 468 'form' => 'Doku_Form', 469 'formid' => 'integer' 470 ) 471 ); 472 $result[] = array( 473 'name' => 'handle_post', 474 'desc' => 'Handle a post to the field', 475 'params' => array( 476 'value' => 'array', 477 'fields' => 'helper_plugin_bureaucracy_field[]', 478 'index' => 'Doku_Form', 479 'formid' => 'integer' 480 ), 481 'return' => array('isvalid' => 'bool') 482 ); 483 $result[] = array( 484 'name' => 'getFieldType', 485 'desc' => 'Get the field type', 486 'return' => array('fieldtype' => 'string') 487 ); 488 $result[] = array( 489 'name' => 'isSet_', 490 'desc' => 'Whether the field is true (used for depending fieldsets) ', 491 'return' => array('isset' => 'bool') 492 ); 493 $result[] = array( 494 'name' => 'getParam', 495 'desc' => 'Get an arbitrary parameter', 496 'params' => array( 497 'name' => 'string' 498 ), 499 'return' => array('Parameter value' => 'mixed|null') 500 ); 501 $result[] = array( 502 'name' => 'after_action', 503 'desc' => 'Executed after performing the action hooks' 504 ); 505 return $result; 506 } 507 508} 509