You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

509 lines
16 KiB

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