MDL-62947 core_form: fix remote code execution exploit in QuickForms
[moodle.git] / lib / pear / HTML / QuickForm / hierselect.php
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$
23 require_once('HTML/QuickForm/group.php');
24 require_once('HTML/QuickForm/select.php');
25 /**
26  * Static utility methods.
27  */
28 require_once 'HTML/QuickForm/utils.php';
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  */
42 class HTML_QuickForm_hierselect extends HTML_QuickForm_group
43 {
44     // {{{ properties
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      *
66      * // If only need two selects
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));
84      *
85      * @var       array
86      * @access    private
87      */
88     var $_options = array();
90     /**
91      * Number of select elements on this group
92      *
93      * @var       int
94      * @access    private
95      */
96     var $_nbElements = 0;
98     /**
99      * The javascript used to set and change the options
100      *
101      * @var       string
102      * @access    private
103      */
104     var $_js = '';
106     // }}}
107     // {{{ constructor
109     /**
110      * Class constructor
111      *
112      * @param     string    $elementName    (optional)Input field name attribute
113      * @param     string    $elementLabel   (optional)Input field label in form
114      * @param     mixed     $attributes     (optional)Either a typical HTML attribute string
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      */
121     public function __construct($elementName=null, $elementLabel=null, $attributes=null, $separator=null) {
122         // TODO MDL-52313 Replace with the call to parent::__construct().
123         HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
124         $this->_persistantFreeze = true;
125         if (isset($separator)) {
126             $this->_separator = $separator;
127         }
128         $this->_type = 'hierselect';
129         $this->_appendName = true;
130     } //end constructor
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     }
142     // }}}
143     // {{{ setOptions()
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;
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 ++) {
166                 $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
167                 $this->_nbElements++;
168             }
169         }
171         $this->_setOptions();
172     } // end func setMainOptions
174     // }}}
175     // {{{ setMainOptions()
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;
190         if (empty($this->_elements)) {
191             $this->_nbElements = 2;
192             $this->_createElements();
193         }
194     } // end func setMainOptions
196     // }}}
197     // {{{ setSecOptions()
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;
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 ++) {
221                 $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
222                 $this->_nbElements++;
223             }
224         }
226         $this->_setOptions();
227     } // end func setSecOptions
229     // }}}
230     // {{{ _setOptions()
232     /**
233      * Sets the options for each select element
234      *
235      * @access    private
236      * @return    void
237      */
238     function _setOptions()
239     {
240         $arrayKeys = [];
241         foreach (array_keys($this->_elements) AS $key) {
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                 }
253             }
254         }
255     } // end func _setOptions
257     // }}}
258     // {{{ setValue()
260     /**
261      * Sets values for group's elements
262      *
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     {
271         // fix for bug #6766. Hope this doesn't break anything more
272         // after bug #7961. Forgot that _nbElements was used in
273         // _createElements() called in several places...
274         $this->_nbElements = max($this->_nbElements, count($value));
275         parent::setValue($value);
276         $this->_setOptions();
277     } // end func setValue
279     // }}}
280     // {{{ _createElements()
282     /**
283      * Creates all the elements for the group
284      *
285      * @access    private
286      * @return    void
287      */
288     function _createElements()
289     {
290         for ($i = 0; $i < $this->_nbElements; $i++) {
291             $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
292         }
293     } // end func _createElements
295     // }}}
296     // {{{ toHtml()
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             }
313             // create the js function to call
314             if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) {
315                 $this->_js .= <<<JAVASCRIPT
316 function _hs_findOptions(ary, keys)
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     }
328 function _hs_findSelect(form, groupName, selectIndex)
330     if (groupName+'['+ selectIndex +']' in form) {
331         return form[groupName+'['+ selectIndex +']'];
332     } else {
333         return form[groupName+'['+ selectIndex +'][]'];
334     }
337 function _hs_unescapeEntities(str)
339     var div = document.createElement('div');
340     div.innerHTML = str;
341     return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
344 function _hs_replaceOptions(ctl, optionList)
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     }
354 function _hs_setValue(ctl, value)
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     }
371 function _hs_swapOptions(form, groupName, selectIndex)
373     var hsValue = [];
374     for (var i = 0; i <= selectIndex; i++) {
375         hsValue[i] = _hs_findSelect(form, groupName, i).value;
376     }
378     _hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1),
379                        _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
380     if (selectIndex + 1 < _hs_options[groupName].length) {
381         _hs_swapOptions(form, groupName, selectIndex + 1);
382     }
385 function _hs_onReset(form, groupNames)
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) {
392                     _hs_replaceOptions(_hs_findSelect(form, groupNames[i], j + 1),
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     }
404 function _hs_setupOnReset(form, groupNames)
406     setTimeout(function() { _hs_onReset(form, groupNames); }, 25);
409 function _hs_onReload()
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     }
422     if (_hs_prevOnload) {
423         _hs_prevOnload();
424     }
427 var _hs_prevOnload = null;
428 if (window.onload) {
429     _hs_prevOnload = window.onload;
431 window.onload = _hs_onReload;
433 var _hs_options = {};
434 var _hs_defaults = {};
436 JAVASCRIPT;
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');
464         $renderer = new HTML_QuickForm_Renderer_Default();
465         $renderer->setElementTemplate('{element}');
466         parent::accept($renderer);
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
478     // }}}
479     // {{{ accept()
481     function accept(&$renderer, $required = false, $error = null)
482     {
483         $renderer->renderElement($this, $required, $error);
484     } // end func accept
486     // }}}
487     // {{{ onQuickFormEvent()
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
514     // }}}
515     // {{{ _convertArrayToJavascript()
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     }
545     // }}}
546     // {{{ _convertScalarToJavascript()
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     }
571     // }}}
572     // {{{ _escapeString()
574    /**
575     * Quotes the string so that it can be used in Javascript string constants
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     }
593     // }}}
594 } // end class HTML_QuickForm_hierselect
595 ?>