MDL-62947 core_form: fix remote code execution exploit in QuickForms
[moodle.git] / lib / pear / HTML / QuickForm / hierselect.php
CommitLineData
da6f8763 1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4: */
3// +----------------------------------------------------------------------+
4// | PHP version 4.0 |
5// +----------------------------------------------------------------------+
6// | Copyright (c) 1997-2004 The PHP Group |
7// +----------------------------------------------------------------------+
8// | This source file is subject to version 2.0 of the PHP license, |
9// | that is bundled with this package in the file LICENSE, and is |
10// | available at through the world-wide-web at |
11// | http://www.php.net/license/2_02.txt. |
12// | If you did not receive a copy of the PHP license and are unable to |
13// | obtain it through the world-wide-web, please send a note to |
14// | license@php.net so we can mail you a copy immediately. |
15// +----------------------------------------------------------------------+
16// | Authors: Herim Vasquez <vasquezh@iro.umontreal.ca> |
17// | Bertrand Mansion <bmansion@mamasam.com> |
18// | Alexey Borzov <avb@php.net>
19// +----------------------------------------------------------------------+
20//
21// $Id$
22
23require_once('HTML/QuickForm/group.php');
24require_once('HTML/QuickForm/select.php');
c87010ba
JD
25/**
26 * Static utility methods.
27 */
28require_once 'HTML/QuickForm/utils.php';
da6f8763 29
30/**
31 * Class to dynamically create two or more HTML Select elements
32 * The first select changes the content of the second select and so on.
33 * This element is considered as a group. Selects will be named
34 * groupName[0], groupName[1], groupName[2]...
35 *
36 * @author Herim Vasquez <vasquezh@iro.umontreal.ca>
37 * @author Bertrand Mansion <bmansion@mamasam.com>
38 * @version 1.0
39 * @since PHP4.04pl1
40 * @access public
41 */
42class HTML_QuickForm_hierselect extends HTML_QuickForm_group
6d504dc0 43{
da6f8763 44 // {{{ properties
45
46 /**
47 * Options for all the select elements
48 *
49 * Format is a bit more complex as we need to know which options
50 * are related to the ones in the previous select:
51 *
52 * Ex:
53 * // first select
54 * $select1[0] = 'Pop';
55 * $select1[1] = 'Classical';
56 * $select1[2] = 'Funeral doom';
57 *
58 * // second select
59 * $select2[0][0] = 'Red Hot Chil Peppers';
60 * $select2[0][1] = 'The Pixies';
61 * $select2[1][0] = 'Wagner';
62 * $select2[1][1] = 'Strauss';
63 * $select2[2][0] = 'Pantheist';
64 * $select2[2][1] = 'Skepticism';
65 *
6d504dc0 66 * // If only need two selects
da6f8763 67 * // - and using the depracated functions
68 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
69 * $sel->setMainOptions($select1);
70 * $sel->setSecOptions($select2);
71 *
72 * // - and using the new setOptions function
73 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
74 * $sel->setOptions(array($select1, $select2));
75 *
76 * // If you have a third select with prices for the cds
77 * $select3[0][0][0] = '15.00$';
78 * $select3[0][0][1] = '17.00$';
79 * etc
80 *
81 * // You can now use
82 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
83 * $sel->setOptions(array($select1, $select2, $select3));
6d504dc0 84 *
da6f8763 85 * @var array
86 * @access private
87 */
88 var $_options = array();
6d504dc0 89
da6f8763 90 /**
91 * Number of select elements on this group
92 *
93 * @var int
94 * @access private
95 */
96 var $_nbElements = 0;
97
98 /**
99 * The javascript used to set and change the options
100 *
101 * @var string
102 * @access private
103 */
104 var $_js = '';
105
106 // }}}
107 // {{{ constructor
108
109 /**
110 * Class constructor
6d504dc0 111 *
da6f8763 112 * @param string $elementName (optional)Input field name attribute
113 * @param string $elementLabel (optional)Input field label in form
6d504dc0 114 * @param mixed $attributes (optional)Either a typical HTML attribute string
da6f8763 115 * or an associative array. Date format is passed along the attributes.
116 * @param mixed $separator (optional)Use a string for one separator,
117 * use an array to alternate the separators.
118 * @access public
119 * @return void
120 */
1a0df553 121 public function __construct($elementName=null, $elementLabel=null, $attributes=null, $separator=null) {
32fada5c 122 // TODO MDL-52313 Replace with the call to parent::__construct().
1a0df553 123 HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
da6f8763 124 $this->_persistantFreeze = true;
125 if (isset($separator)) {
126 $this->_separator = $separator;
127 }
128 $this->_type = 'hierselect';
129 $this->_appendName = true;
130 } //end constructor
131
1a0df553
MG
132 /**
133 * Old syntax of class constructor. Deprecated in PHP7.
134 *
135 * @deprecated since Moodle 3.1
136 */
137 public function HTML_QuickForm_hierselect($elementName=null, $elementLabel=null, $attributes=null, $separator=null) {
138 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
139 self::__construct($elementName, $elementLabel, $attributes, $separator);
140 }
141
da6f8763 142 // }}}
143 // {{{ setOptions()
144
145 /**
146 * Initialize the array structure containing the options for each select element.
147 * Call the functions that actually do the magic.
148 *
149 * @param array $options Array of options defining each element
150 *
151 * @access public
152 * @return void
153 */
154 function setOptions($options)
155 {
156 $this->_options = $options;
157
158 if (empty($this->_elements)) {
159 $this->_nbElements = count($this->_options);
160 $this->_createElements();
161 } else {
162 // setDefaults has probably been called before this function
163 // check if all elements have been created
164 $totalNbElements = count($this->_options);
165 for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
6d504dc0 166 $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
da6f8763 167 $this->_nbElements++;
168 }
169 }
6d504dc0 170
da6f8763 171 $this->_setOptions();
172 } // end func setMainOptions
173
174 // }}}
175 // {{{ setMainOptions()
6d504dc0 176
da6f8763 177 /**
178 * Sets the options for the first select element. Deprecated. setOptions() should be used.
179 *
180 * @param array $array Options for the first select element
181 *
182 * @access public
183 * @deprecated Deprecated since release 3.2.2
184 * @return void
185 */
186 function setMainOptions($array)
187 {
188 $this->_options[0] = $array;
189
190 if (empty($this->_elements)) {
191 $this->_nbElements = 2;
192 $this->_createElements();
193 }
194 } // end func setMainOptions
6d504dc0 195
da6f8763 196 // }}}
197 // {{{ setSecOptions()
6d504dc0 198
da6f8763 199 /**
200 * Sets the options for the second select element. Deprecated. setOptions() should be used.
201 * The main _options array is initialized and the _setOptions function is called.
202 *
203 * @param array $array Options for the second select element
204 *
205 * @access public
206 * @deprecated Deprecated since release 3.2.2
207 * @return void
208 */
209 function setSecOptions($array)
210 {
211 $this->_options[1] = $array;
212
213 if (empty($this->_elements)) {
214 $this->_nbElements = 2;
215 $this->_createElements();
216 } else {
217 // setDefaults has probably been called before this function
218 // check if all elements have been created
219 $totalNbElements = 2;
220 for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
6d504dc0 221 $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
da6f8763 222 $this->_nbElements++;
223 }
224 }
6d504dc0 225
da6f8763 226 $this->_setOptions();
227 } // end func setSecOptions
6d504dc0 228
da6f8763 229 // }}}
230 // {{{ _setOptions()
6d504dc0 231
da6f8763 232 /**
233 * Sets the options for each select element
234 *
235 * @access private
236 * @return void
237 */
238 function _setOptions()
239 {
c87010ba 240 $arrayKeys = [];
da6f8763 241 foreach (array_keys($this->_elements) AS $key) {
c87010ba
JD
242 if (isset($this->_options[$key])) {
243 if ((empty($arrayKeys)) || HTML_QuickForm_utils::recursiveIsset($this->_options[$key], $arrayKeys)) {
244 $array = empty($arrayKeys) ? $this->_options[$key] : HTML_QuickForm_utils::recursiveValue($this->_options[$key], $arrayKeys);
245 if (is_array($array)) {
246 $select =& $this->_elements[$key];
247 $select->_options = array();
248 $select->loadArray($array);
249 $value = is_array($v = $select->getValue()) ? $v[0] : key($array);
250 $arrayKeys[] = $value;
251 }
252 }
da6f8763 253 }
254 }
255 } // end func _setOptions
6d504dc0 256
da6f8763 257 // }}}
258 // {{{ setValue()
259
260 /**
261 * Sets values for group's elements
6d504dc0 262 *
da6f8763 263 * @param array $value An array of 2 or more values, for the first,
264 * the second, the third etc. select
265 *
266 * @access public
267 * @return void
268 */
269 function setValue($value)
270 {
6d504dc0 271 // fix for bug #6766. Hope this doesn't break anything more
da6f8763 272 // after bug #7961. Forgot that _nbElements was used in
6d504dc0 273 // _createElements() called in several places...
da6f8763 274 $this->_nbElements = max($this->_nbElements, count($value));
275 parent::setValue($value);
276 $this->_setOptions();
277 } // end func setValue
6d504dc0 278
da6f8763 279 // }}}
280 // {{{ _createElements()
281
282 /**
283 * Creates all the elements for the group
6d504dc0 284 *
da6f8763 285 * @access private
286 * @return void
287 */
288 function _createElements()
289 {
290 for ($i = 0; $i < $this->_nbElements; $i++) {
6d504dc0 291 $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
da6f8763 292 }
293 } // end func _createElements
294
295 // }}}
296 // {{{ toHtml()
297
298 function toHtml()
299 {
300 $this->_js = '';
301 if (!$this->_flagFrozen) {
302 // set the onchange attribute for each element except last
303 $keys = array_keys($this->_elements);
304 $onChange = array();
305 for ($i = 0; $i < count($keys) - 1; $i++) {
306 $select =& $this->_elements[$keys[$i]];
307 $onChange[$i] = $select->getAttribute('onchange');
308 $select->updateAttributes(
309 array('onchange' => '_hs_swapOptions(this.form, \'' . $this->_escapeString($this->getName()) . '\', ' . $keys[$i] . ');' . $onChange[$i])
310 );
311 }
6d504dc0 312
da6f8763 313 // create the js function to call
314 if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) {
315 $this->_js .= <<<JAVASCRIPT
316function _hs_findOptions(ary, keys)
317{
318 var key = keys.shift();
319 if (!key in ary) {
320 return {};
321 } else if (0 == keys.length) {
322 return ary[key];
323 } else {
324 return _hs_findOptions(ary[key], keys);
325 }
326}
327
328function _hs_findSelect(form, groupName, selectIndex)
329{
330 if (groupName+'['+ selectIndex +']' in form) {
6d504dc0 331 return form[groupName+'['+ selectIndex +']'];
da6f8763 332 } else {
6d504dc0 333 return form[groupName+'['+ selectIndex +'][]'];
da6f8763 334 }
335}
336
337function _hs_unescapeEntities(str)
338{
339 var div = document.createElement('div');
340 div.innerHTML = str;
341 return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
342}
343
344function _hs_replaceOptions(ctl, optionList)
345{
346 var j = 0;
347 ctl.options.length = 0;
348 for (i in optionList) {
349 var optionText = (-1 == optionList[i].indexOf('&'))? optionList[i]: _hs_unescapeEntities(optionList[i]);
350 ctl.options[j++] = new Option(optionText, i, false, false);
351 }
352}
353
354function _hs_setValue(ctl, value)
355{
356 var testValue = {};
357 if (value instanceof Array) {
358 for (var i = 0; i < value.length; i++) {
359 testValue[value[i]] = true;
360 }
361 } else {
362 testValue[value] = true;
363 }
364 for (var i = 0; i < ctl.options.length; i++) {
365 if (ctl.options[i].value in testValue) {
366 ctl.options[i].selected = true;
367 }
368 }
369}
370
371function _hs_swapOptions(form, groupName, selectIndex)
372{
373 var hsValue = [];
374 for (var i = 0; i <= selectIndex; i++) {
375 hsValue[i] = _hs_findSelect(form, groupName, i).value;
376 }
377
6d504dc0 378 _hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1),
da6f8763 379 _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
380 if (selectIndex + 1 < _hs_options[groupName].length) {
381 _hs_swapOptions(form, groupName, selectIndex + 1);
382 }
383}
384
385function _hs_onReset(form, groupNames)
386{
387 for (var i = 0; i < groupNames.length; i++) {
388 try {
389 for (var j = 0; j <= _hs_options[groupNames[i]].length; j++) {
390 _hs_setValue(_hs_findSelect(form, groupNames[i], j), _hs_defaults[groupNames[i]][j]);
391 if (j < _hs_options[groupNames[i]].length) {
6d504dc0 392 _hs_replaceOptions(_hs_findSelect(form, groupNames[i], j + 1),
da6f8763 393 _hs_findOptions(_hs_options[groupNames[i]][j], _hs_defaults[groupNames[i]].slice(0, j + 1)));
394 }
395 }
396 } catch (e) {
397 if (!(e instanceof TypeError)) {
398 throw e;
399 }
400 }
401 }
402}
403
404function _hs_setupOnReset(form, groupNames)
405{
406 setTimeout(function() { _hs_onReset(form, groupNames); }, 25);
407}
408
409function _hs_onReload()
410{
411 var ctl;
412 for (var i = 0; i < document.forms.length; i++) {
413 for (var j in _hs_defaults) {
414 if (ctl = _hs_findSelect(document.forms[i], j, 0)) {
415 for (var k = 0; k < _hs_defaults[j].length; k++) {
416 _hs_setValue(_hs_findSelect(document.forms[i], j, k), _hs_defaults[j][k]);
417 }
418 }
419 }
420 }
421
422 if (_hs_prevOnload) {
423 _hs_prevOnload();
424 }
425}
426
427var _hs_prevOnload = null;
428if (window.onload) {
429 _hs_prevOnload = window.onload;
430}
431window.onload = _hs_onReload;
432
433var _hs_options = {};
434var _hs_defaults = {};
435
436JAVASCRIPT;
437 define('HTML_QUICKFORM_HIERSELECT_EXISTS', true);
438 }
439 // option lists
440 $jsParts = array();
441 for ($i = 1; $i < $this->_nbElements; $i++) {
442 $jsParts[] = $this->_convertArrayToJavascript($this->_options[$i]);
443 }
444 $this->_js .= "\n_hs_options['" . $this->_escapeString($this->getName()) . "'] = [\n" .
445 implode(",\n", $jsParts) .
446 "\n];\n";
447 // default value; if we don't actually have any values yet just use
448 // the first option (for single selects) or empty array (for multiple)
449 $values = array();
450 foreach (array_keys($this->_elements) as $key) {
451 if (is_array($v = $this->_elements[$key]->getValue())) {
452 $values[] = count($v) > 1? $v: $v[0];
453 } else {
454 // XXX: accessing the supposedly private _options array
455 $values[] = $this->_elements[$key]->getMultiple() || empty($this->_elements[$key]->_options[0])?
456 array():
457 $this->_elements[$key]->_options[0]['attr']['value'];
458 }
459 }
460 $this->_js .= "_hs_defaults['" . $this->_escapeString($this->getName()) . "'] = " .
461 $this->_convertArrayToJavascript($values, false) . ";\n";
462 }
463 include_once('HTML/QuickForm/Renderer/Default.php');
6d504dc0 464 $renderer = new HTML_QuickForm_Renderer_Default();
da6f8763 465 $renderer->setElementTemplate('{element}');
466 parent::accept($renderer);
467
468 if (!empty($onChange)) {
469 $keys = array_keys($this->_elements);
470 for ($i = 0; $i < count($keys) - 1; $i++) {
471 $this->_elements[$keys[$i]]->updateAttributes(array('onchange' => $onChange[$i]));
472 }
473 }
474 return (empty($this->_js)? '': "<script type=\"text/javascript\">\n//<![CDATA[\n" . $this->_js . "//]]>\n</script>") .
475 $renderer->toHtml();
476 } // end func toHtml
477
478 // }}}
479 // {{{ accept()
480
481 function accept(&$renderer, $required = false, $error = null)
482 {
483 $renderer->renderElement($this, $required, $error);
484 } // end func accept
485
486 // }}}
487 // {{{ onQuickFormEvent()
488
489 function onQuickFormEvent($event, $arg, &$caller)
490 {
491 if ('updateValue' == $event) {
492 // we need to call setValue() so that the secondary option
493 // matches the main option
494 return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller);
495 } else {
496 $ret = parent::onQuickFormEvent($event, $arg, $caller);
497 // add onreset handler to form to properly reset hierselect (see bug #2970)
498 if ('addElement' == $event) {
499 $onReset = $caller->getAttribute('onreset');
500 if (strlen($onReset)) {
501 if (strpos($onReset, '_hs_setupOnReset')) {
502 $caller->updateAttributes(array('onreset' => str_replace('_hs_setupOnReset(this, [', "_hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "', ", $onReset)));
503 } else {
504 $caller->updateAttributes(array('onreset' => "var temp = function() { {$onReset} } ; if (!temp()) { return false; } ; if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
505 }
506 } else {
507 $caller->updateAttributes(array('onreset' => "if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
508 }
509 }
510 return $ret;
511 }
512 } // end func onQuickFormEvent
513
514 // }}}
515 // {{{ _convertArrayToJavascript()
516
517 /**
518 * Converts PHP array to its Javascript analog
519 *
520 * @access private
521 * @param array PHP array to convert
522 * @param bool Generate Javascript object literal (default, works like PHP's associative array) or array literal
523 * @return string Javascript representation of the value
524 */
525 function _convertArrayToJavascript($array, $assoc = true)
526 {
527 if (!is_array($array)) {
528 return $this->_convertScalarToJavascript($array);
529 } else {
530 $items = array();
531 foreach ($array as $key => $val) {
532 $item = $assoc? "'" . $this->_escapeString($key) . "': ": '';
533 if (is_array($val)) {
534 $item .= $this->_convertArrayToJavascript($val, $assoc);
535 } else {
536 $item .= $this->_convertScalarToJavascript($val);
537 }
538 $items[] = $item;
539 }
540 }
541 $js = implode(', ', $items);
542 return $assoc? '{ ' . $js . ' }': '[' . $js . ']';
543 }
6d504dc0 544
da6f8763 545 // }}}
546 // {{{ _convertScalarToJavascript()
547
548 /**
549 * Converts PHP's scalar value to its Javascript analog
550 *
551 * @access private
552 * @param mixed PHP value to convert
553 * @return string Javascript representation of the value
554 */
555 function _convertScalarToJavascript($val)
556 {
557 if (is_bool($val)) {
558 return $val ? 'true' : 'false';
559 } elseif (is_int($val) || is_double($val)) {
560 return $val;
561 } elseif (is_string($val)) {
562 return "'" . $this->_escapeString($val) . "'";
563 } elseif (is_null($val)) {
564 return 'null';
565 } else {
566 // don't bother
567 return '{}';
568 }
569 }
570
571 // }}}
572 // {{{ _escapeString()
573
574 /**
6d504dc0 575 * Quotes the string so that it can be used in Javascript string constants
da6f8763 576 *
577 * @access private
578 * @param string
579 * @return string
580 */
581 function _escapeString($str)
582 {
583 return strtr($str,array(
584 "\r" => '\r',
585 "\n" => '\n',
586 "\t" => '\t',
587 "'" => "\\'",
588 '"' => '\"',
589 '\\' => '\\\\'
590 ));
591 }
592
593 // }}}
594} // end class HTML_QuickForm_hierselect
c87010ba 595?>