@@ -134,7 +134,7 @@ public function render(): render_result {
134134 mt_srand ($ nextseed );
135135 }
136136
137- $ warnings = $ this ->check_for_unknown_options ($ availableoptions );
137+ $ warnings = $ this ->check_for_and_preserve_unknown_options ($ availableoptions );
138138 $ this ->result = new render_result ($ this ->xml ->saveHTML (), $ warnings );
139139 return $ this ->result ;
140140 }
@@ -595,11 +595,11 @@ function (array $match) use ($question) {
595595 * - While we should discourage it, it is possible for inputs to be inside `qpy:if-role` or `qpy:feedback`
596596 * elements. {@see question_ui_metadata_extractor} doesn't resolve those.
597597 *
598- * @return array
599- * @see check_for_unknown_options
598+ * @return array<string, available_opts_info>
599+ * @see check_for_and_preserve_unknown_options
600600 */
601601 private function extract_available_options (): array {
602- $ optionsbyname = [];
602+ $ infobyname = [];
603603
604604 /** @var DOMElement $select */
605605 foreach ($ this ->xpath ->query ('//xhtml:select[not(@qpy:warn-on-unknown-option = "no")] ' ) as $ select ) {
@@ -608,69 +608,72 @@ private function extract_available_options(): array {
608608 continue ;
609609 }
610610
611- $ values = [];
611+ $ optvalues = [];
612612 /** @var DOMElement $option */
613613 foreach ($ this ->xpath ->query ('./xhtml:option | ./xhtml:optgroup/xhtml:option ' , $ select ) as $ option ) {
614- $ values [] = $ option ->hasAttribute ('value ' ) ? $ option ->getAttribute ('value ' ) : $ option ->textContent ;
614+ $ optvalues [] = $ option ->hasAttribute ('value ' ) ? $ option ->getAttribute ('value ' ) : $ option ->textContent ;
615615 }
616616
617- $ optionsbyname [$ name ] = array_unique ($ values );
617+ $ warn = $ select ->getAttributeNS (constants::NAMESPACE_QPY , 'warn-on-unknown-option ' ) !== 'no ' ;
618+
619+ $ infobyname [$ name ] = new available_opts_info ('select ' , array_unique ($ optvalues ), $ warn );
618620 }
619621
620- $ ignorednames = [];
621622 /** @var DOMElement $input */
622623 foreach ($ this ->xpath ->query ('//xhtml:input[(@type="checkbox" or @type="radio")] ' ) as $ input ) {
623624 $ name = $ input ->getAttribute ('name ' );
624625 if (!$ name ) {
625626 continue ;
626627 }
627- if (in_array ($ name , $ ignorednames )) {
628- continue ;
629- }
630- if ($ input ->getAttributeNS (constants::NAMESPACE_QPY , 'warn-on-unknown-option ' ) === 'no ' ) {
631- $ ignorednames [] = $ name ;
632- continue ;
633- }
634628
635- if (!array_key_exists ($ name , $ optionsbyname )) {
636- $ optionsbyname [$ name ] = [];
629+ $ info = $ infobyname [$ name ] ??= new available_opts_info ($ input ->getAttribute ('type ' ), [], true );
630+
631+ if ($ input ->getAttributeNS (constants::NAMESPACE_QPY , 'warn-on-unknown-option ' ) === 'no ' ) {
632+ $ info ->warnonunknownoption = false ;
637633 }
638634
639635 $ value = $ input ->hasAttribute ('value ' ) ? $ input ->getAttribute ('value ' ) : 'on ' ;
640- if (!in_array ($ value , $ optionsbyname [ $ name ] )) {
641- $ optionsbyname [ $ name ] [] = $ value ;
636+ if (!in_array ($ value , $ info -> availableoptions )) {
637+ $ info -> availableoptions [] = $ value ;
642638 }
643639 }
644640
645- foreach ($ ignorednames as $ ignoredname ) {
646- unset($ optionsbyname [$ ignoredname ]);
647- }
648- foreach ($ optionsbyname as &$ values ) {
649- sort ($ values );
641+ foreach ($ infobyname as $ info ) {
642+ sort ($ info ->availableoptions );
650643 }
651644
652- return $ optionsbyname ;
645+ return $ infobyname ;
653646 }
654647
655648 /**
656- * Checks if the last response contains values which are invalid.
649+ * Checks the last response for invalid values and adds hidden inputs to preserve those invalid values .
657650 *
658- * @param array $availableoptionsbyname
659- * @return array
651+ * @param available_opts_info[] $availableoptsinfobyname
652+ * @return invalid_option_warning[]
653+ * @throws coding_exception
660654 * @see extract_available_options
661655 */
662- private function check_for_unknown_options (array $ availableoptionsbyname ): array {
656+ private function check_for_and_preserve_unknown_options (array $ availableoptsinfobyname ): array {
663657 $ response = utils::get_last_response ($ this ->attempt );
664658
665659 $ warnings = [];
666- foreach ($ availableoptionsbyname as $ name => $ availableoptions ) {
660+ foreach ($ availableoptsinfobyname as $ name => $ info ) {
661+ if (!$ info ->warnonunknownoption ) {
662+ continue ;
663+ }
667664 if (!array_key_exists ($ name , $ response )) {
668665 continue ;
669666 }
670667
671668 $ lastvalue = $ response [$ name ];
672- if (!in_array ($ lastvalue , $ availableoptions )) {
673- $ warnings [] = new invalid_option_warning ($ name , $ lastvalue , $ availableoptions );
669+ if (in_array ($ lastvalue , $ info ->availableoptions )) {
670+ continue ;
671+ }
672+
673+ $ warnings [] = new invalid_option_warning ($ name , $ lastvalue , $ info ->availableoptions );
674+ if ($ info ->type !== 'select ' ) {
675+ // Selects are handled in dom_utils::set_select_value.
676+ dom_utils::add_hidden_input ($ this ->xml ->documentElement , $ this ->attempt ->get_qt_field_name ($ name ), $ lastvalue );
674677 }
675678 }
676679 return $ warnings ;
0 commit comments