r100690 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r100689‎ | r100690 | r100691 >
Date:11:52, 25 October 2011
Author:questpc
Status:deferred (Comments)
Tags:
Comment:
Fixed regression in question type text when prefilled text inputs were not displayed. Added textarea and select multiple view modes for question type text categories via introduced height and multiple category attributes. Allow unquoted integer value for xml-like attributes.
Modified paths:
  • /trunk/extensions/QPoll/clientside/qp_user.js (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_textquestion.php (modified) (history)
  • /trunk/extensions/QPoll/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/specials/qp_results.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_textquestionview.php (modified) (history)

Diff [purge]

Index: trunk/extensions/QPoll/clientside/qp_user.js
@@ -48,7 +48,7 @@
4949 /**
5050 * Parses coordinate of poll's input stored in id and
5151 * stores it into self.catCoord;
52 - * @param id id attribute of input / select element
 52+ * @param id id attribute of input / textarea / select element
5353 * @return true, when value of id has valid coordinate, false otherwise.
5454 */
5555 setCatCoord : function( id ) {
@@ -87,9 +87,14 @@
8888 applyRadio : function( catElem ) {
8989 if ( self.radioIsClicked ) {
9090 // deselect all inputs
91 - if ( catElem.nodeName == 'SELECT' || catElem.type == 'text' ) {
 91+ if ( catElem.nodeName != 'INPUT' || catElem.type == 'text' ) {
 92+ // text controls
9293 catElem.value = '';
 94+ if ( catElem.nodeName == 'TEXTAREA' ) {
 95+ catElem.innerHTML = '';
 96+ }
9397 } else {
 98+ // switching controls
9499 catElem.checked = false;
95100 }
96101 } else {
@@ -160,6 +165,15 @@
161166 }
162167 },
163168
 169+ setTextRowHandler : function( parent, tagName ) {
 170+ var tags = parent.getElementsByTagName( tagName );
 171+ for ( j = 0; j < tags.length; j++ ) {
 172+ if ( tags[j].id && tags[j].id.slice( 0, 2 ) == 'tx' ) {
 173+ addEvent( tags[j], "click", self.clickTextRow );
 174+ }
 175+ }
 176+ },
 177+
164178 /**
165179 * Prepare the Poll for "javascriptable" browsers
166180 */
@@ -198,13 +212,8 @@
199213 }
200214 }
201215 }
202 - var select = bodyContentDiv[i].getElementsByTagName( 'select' );
203 - for ( j = 0; j < select.length; j++ ) {
204 - // selects currently are used only with question type="text", type="text!"
205 - if ( select[j].id && select[j].id.slice( 0, 2 ) == 'tx' ) {
206 - addEvent( select[j], "click", self.clickTextRow );
207 - }
208 - }
 216+ self.setTextRowHandler( bodyContentDiv[i], 'select' );
 217+ self.setTextRowHandler( bodyContentDiv[i], 'textarea' );
209218 }
210219 }
211220 };
Index: trunk/extensions/QPoll/specials/qp_results.php
@@ -313,7 +313,12 @@
314314 @unlink( $xls_fname );
315315 exit();
316316 } catch ( Exception $e ) {
317 - die( "Error while exporting poll statistics to Excel table\n" );
 317+ if ( $e instanceof MWException ) {
 318+ $e->reportHTML();
 319+ exit();
 320+ } else {
 321+ die( "Error while exporting poll statistics to Excel table\n" );
 322+ }
318323 }
319324 }
320325
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php
@@ -23,23 +23,34 @@
2424
2525 # whether the current option has xml-like attributes specified
2626 var $hasAttributes = false;
 27+ ## Category attributes;
 28+ # Defined as xml-like attribute in the first element of options list.
2729 var $attributes = array(
28 - ## a value of input text field width in 'em'
29 - # possible values: null, positive int
30 - # defined as first element xml-like attribute of options list, for example:
31 - # <<:: width="12">> or <<:: width="15"|test>>
32 - # currently, it is used only for text inputs (not for select/option list)
 30+ ## width of input text field
 31+ # possible values: null, positive int, 'auto'
 32+ # <<:: width="12">> or <<:: width="15"|test>> or <<:: width="auto"|asd|fgh>>
 33+ # currently, it is used only for text input / textarea (not for select/option list)
3334 'width' => null,
 35+ ## number of text lines in an select / textarea
 36+ # possible values: null, positive int, 'auto'
 37+ # when there is no options or only one option, it produces an textarea
 38+ # when there is more than one option, it produces a scrollable select / options list
 39+ # value 'auto' is meaningful only when there are more than one option given;
 40+ # <<:: height="4">> or <<:: height="10"|prefilled text>>
 41+ 'height' => null,
3442 ## whether the text options of current category has to be sorted;
35 - # possible values: null (do not sort), 'asc', 'desc'
36 - # defined as first element xml-like attribute of options list, for example:
 43+ # possible values: null (do not sort), 'asc', 'desc'
3744 # <<:: sorting="desc"|a|b|c>>
3845 'sorting' => null,
3946 ## whether the checkbox type option of current category has to be checked by default;
40 - # possible value: null (not checked), not null (checked)
41 - # defined as first element xml-like attribute of options list, for example:
 47+ # possible values: null (not checked), not null (checked)
4248 # <[checked=""]>
43 - 'checked' => null
 49+ 'checked' => null,
 50+ ## whether the select for current category can have multiple options selected
 51+ # possible values: null (no multiple selection), not null (multiple selection)
 52+ # it is meaningful only for text categories with multiple options defined:
 53+ # <<:: multiple=""|1|2|3>>
 54+ 'multiple' => null
4455 );
4556 # a pointer to last element in $this->input_options array
4657 var $iopt_last;
@@ -103,7 +114,7 @@
104115 # because it is checked in $this->addEmptyOption()
105116 $this->hasAttributes = true;
106117 # parse attributes string
107 - $option_attributes = qp_Setup::getXmlLikeAttributes( $matches[1], array( 'width', 'sorting' ) );
 118+ $option_attributes = qp_Setup::getXmlLikeAttributes( $matches[1], array( 'width', 'height', 'sorting', 'multiple' ) );
108119 # apply attributes to current option
109120 foreach ( $option_attributes as $attr_name => $attr_val ) {
110121 $this->attributes[$attr_name] = $attr_val;
@@ -256,14 +267,21 @@
257268 function loadProposalCategory( qp_TextQuestionOptions $opt, $proposalId, $catId ) {
258269 global $wgContLang;
259270 $name = "q{$this->mQuestionId}p{$proposalId}s{$catId}";
260 - # default value for unanswered category
261 - # boolean true "checked" checkbox / radiobutton
262 - # string - text input / select option value
263 - $text_answer = false;
 271+ $answered = false;
 272+ $text_answer = '';
264273 # try to load from POST data
265 - if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) !== null ) {
 274+ if ( $this->poll->mBeingCorrected &&
 275+ ( $ta = $this->mRequest->getArray( $name ) ) !== null ) {
266276 if ( $opt->type === 'text' ) {
267 - if ( ( $ta = trim( $this->mRequest->getText( $name ) ) ) != '' ) {
 277+ if ( count( $ta ) === 1 ) {
 278+ # fallback to WebRequest::getText(), because it offers useful preprocessing
 279+ $ta = trim( $this->mRequest->getText( $name ) );
 280+ } else {
 281+ # select multiple values are separated with new lines
 282+ $ta = implode( "\n", array_map( 'trim', $ta ) );
 283+ }
 284+ if ( $ta != '' ) {
 285+ $answered = true;
268286 if ( strlen( $ta ) > qp_Setup::$field_max_len['text_answer'] ) {
269287 $text_answer = $wgContLang->truncate( $ta, qp_Setup::$field_max_len['text_answer'] , '' );
270288 } else {
@@ -271,34 +289,28 @@
272290 }
273291 }
274292 } else {
275 - $text_answer = true;
 293+ $answered = true;
276294 }
277295 }
278296 # try to load from pollStore
279297 # pollStore optionally overrides POST data
280 - $prev_text_answer = $this->answerExists( $opt->type, $proposalId, $catId );
281 - if ( is_string( $prev_text_answer ) ) {
282 - $text_answer = $prev_text_answer;
283 - } else {
284 - if ( $prev_text_answer === true ) {
285 - $text_answer = true;
 298+ if ( ( $prev_text_answer = $this->answerExists( $opt->type, $proposalId, $catId ) ) !== false ) {
 299+ $answered = true;
 300+ if ( is_string( $prev_text_answer ) ) {
 301+ $text_answer = $prev_text_answer;
286302 }
287303 }
288 - if ( $text_answer !== false ) {
 304+ if ( $answered !== false ) {
289305 # add category to the list of user answers for current proposal (row)
290306 $this->mProposalCategoryId[ $proposalId ][] = $catId;
291 - if ( is_string( $text_answer ) ) {
292 - $this->mProposalCategoryText[ $proposalId ][] = $text_answer;
293 - } else {
294 - $this->mProposalCategoryText[ $proposalId ][] = '';
295 - if ( $opt->type !== 'text' ) {
296 - $opt->attributes['checked'] = true;
297 - }
 307+ $this->mProposalCategoryText[ $proposalId ][] = $text_answer;
 308+ if ( $opt->type !== 'text' ) {
 309+ $opt->attributes['checked'] = true;
298310 }
299311 }
300312 # finally, add new category input options for the view
301313 $opt->closeCategory();
302 - $this->propview->addCatDef( $opt, $name, $text_answer, $this->poll->mBeingCorrected && $text_answer === false );
 314+ $this->propview->addCatDef( $opt, $name, $text_answer, $this->poll->mBeingCorrected && !$answered );
303315 }
304316
305317 /**
Index: trunk/extensions/QPoll/qp_user.php
@@ -469,8 +469,9 @@
470470 $attr_vals = array();
471471 $match = array();
472472 foreach ( $attr_list as $attr_name ) {
473 - preg_match( '/' . $attr_name . '\s?=\s?"(.*?)"/u', $attr_str, $match );
474 - $attr_vals[$attr_name] = ( count( $match ) > 1 ) ? $match[1] : null;
 473+ preg_match( '/' . $attr_name . '\s?=\s?(?:"(.*?)"|(\d+))/u', $attr_str, $match );
 474+ # array_pop() "prefers" to match (\d+), when available
 475+ $attr_vals[$attr_name] = ( count( $match ) > 1 ) ? array_pop( $match ) : null;
475476 }
476477 return $attr_vals;
477478 }
Index: trunk/extensions/QPoll/view/proposal/qp_textquestionproposalview.php
@@ -45,6 +45,8 @@
4646 * @param $name string name of input/select element (used in the view)
4747 * @param $text_answer string user's POSTed category answer
4848 * (empty string '' means no answer)
 49+ * @param $unanswered boolean indicates whether the category of submitted poll
 50+ * was non-blank (true) or not (false)
4951 * @return stdClass object with viewtokens entry
5052 */
5153 function addCatDef( qp_TextQuestionOptions $opt, $name, $text_answer, $unanswered ) {
@@ -57,10 +59,7 @@
5860 # property 'value' contains value previousely chosen
5961 # by user (if any)
6062 # property 'attributes' contain extra atttibutes of current category definition
61 - # property 'unanswered' boolean
62 - # true - the question was POSTed but category is unanswered
63 - # false - the question was not POSTed or category is answered
64 - $this->viewtokens[] = (object) array(
 63+ $viewtoken = (object) array(
6564 'type' => $opt->type,
6665 'options' => $opt->input_options,
6766 'name' => $name,
@@ -68,6 +67,21 @@
6968 'unanswered' => $unanswered,
7069 'attributes' => $opt->attributes
7170 );
 71+ # fix values of measurable attributes (allow only non-negative integer values)
 72+ # zero value means attribute is unused
 73+ foreach ( array( 'width', 'height' ) as $measurable ) {
 74+ $val = &$viewtoken->attributes[$measurable];
 75+ if ( $val === null ) {
 76+ $val = 0;
 77+ } elseif ( is_numeric( $val ) ) {
 78+ if ( ( $val = intval( $val ) ) < 1 ) {
 79+ $val = 0;
 80+ }
 81+ } else {
 82+ $val = 'auto';
 83+ }
 84+ }
 85+ $this->viewtokens[] = $viewtoken;
7286 $this->lastTokenType = 'category';
7387 }
7488
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -51,6 +51,7 @@
5252 # depending on $this->tabularDisplay value
5353 var $row;
5454 # tagarray with error elements will be merged into adjascent cells
 55+ # (otherwise tabular layout will be broken by proposal errors)
5556 var $error;
5657 # tagarray with current cell builded for row
5758 # cell contains one or multiple tags, describing proposal part or category
@@ -63,6 +64,9 @@
6465 $this->reset( '' );
6566 }
6667
 68+ /**
 69+ * Prepare current instance for new proposal row
 70+ */
6771 function reset( $id_prefix ) {
6872 $this->id_prefix = $id_prefix;
6973 $this->row = array();
@@ -72,6 +76,9 @@
7377 $this->ckey = 0;
7478 }
7579
 80+ /**
 81+ * Add proposal error tagarray
 82+ */
7683 function addError( $elem ) {
7784 $this->cell[] = array(
7885 '__tag' => 'span',
@@ -80,57 +87,98 @@
8188 );
8289 }
8390
 91+ /**
 92+ * Add category as input type text / checkbox / radio / textarea tagarray
 93+ */
8494 function addInput( $elem, $className ) {
 95+ $tagName = ( $elem->type === 'text' && $elem->attributes['height'] !== 0 ) ? 'textarea' : 'input';
 96+ $lines_count = 1;
 97+ # get category value
8598 $value = $elem->value;
8699 # check, whether the definition of category has "pre-filled" value
87100 # single, non-unanswered, non-empty option is a pre-filled value
88101 if ( !$elem->unanswered && $elem->value === '' && $elem->options[0] !== '' ) {
89102 # input text pre-fill
90103 $value = $elem->options[0];
 104+ if ( $tagName === 'textarea' ) {
 105+ # oversimplicated regexp, but it's enough for our needs
 106+ $value = preg_replace( '/<br[\sA-Z\d="]*\/{0,1}>/i', "\n", $value, -1, $lines_count );
 107+ $lines_count++;
 108+ }
91109 $className .= ' cat_prefilled';
92110 }
93 - $input = array(
94 - '__tag' => 'input',
 111+ $tag = array(
 112+ '__tag' => $tagName,
95113 # unique (poll_type,order_id,question,proposal,category) "coordinate" for javascript
96114 'id' => "{$this->id_prefix}c{$this->ckey}",
97115 'class' => $className,
98 - 'type' => $elem->type,
99116 'name' => $elem->name,
100 - 'value' => qp_Setup::specialchars( $value )
101117 );
 118+ if ( $tagName === 'input' ) {
 119+ $tag['type'] = $elem->type;
 120+ $tag['value'] = qp_Setup::specialchars( $value );
 121+ } else { /* 'textarea' */
 122+ $tag[] = qp_Setup::specialchars( $value );
 123+ if ( is_int( $elem->attributes['height'] ) ) {
 124+ $tag['rows'] = $elem->attributes['height'];
 125+ } else { /* 'auto' */
 126+ # todo: allow multiline prefilled text and calculate number of new lines
 127+ $tag['rows'] = $lines_count;
 128+ }
 129+ }
102130 $this->ckey++;
103 - if ( $elem->type !== 'text' && $elem->attributes['checked'] === true ) {
104 - $input['checked'] = 'checked';
 131+ if ( $elem->type === 'text' ) {
 132+ # input type text and textarea
 133+ if ( $this->owner->textInputStyle != '' ) {
 134+ # apply poll's textwidth attribute
 135+ $tag['style'] = $this->owner->textInputStyle;
 136+ }
 137+ if ( $elem->attributes['width'] !== 0 ) {
 138+ # apply current category width attribute
 139+ if ( is_int( $elem->attributes['width'] ) ) {
 140+ $tag['style'] = 'width:' . $elem->attributes['width'] . 'em;';
 141+ } else { /* 'auto' */
 142+ $tag['style'] = 'width:99%;';
 143+ }
 144+ }
 145+ } else {
 146+ # checkbox or radiobutton
 147+ if ( $elem->attributes['checked'] === true ) {
 148+ $tag['checked'] = 'checked';
 149+ }
105150 }
106 - if ( $elem->type === 'text' && $this->owner->textInputStyle != '' ) {
107 - # apply poll's textwidth attribute
108 - $input['style'] = $this->owner->textInputStyle;
109 - }
110 - if ( $elem->attributes['width'] !== null ) {
111 - # apply current category width attribute
112 - $input['style'] = 'width:' . intval( $elem->attributes['width'] ) . 'em;';
113 - }
114 - $this->cell[] = $input;
 151+ $this->cell[] = $tag;
115152 }
116153
 154+ /**
 155+ * Add category as select / option list tagarray
 156+ */
117157 function addSelect( $elem, $className ) {
118158 if ( $elem->options[0] !== '' ) {
119 - # default element in select/option set always must be empty option
 159+ # default element in select/option set always must be an empty option
120160 array_unshift( $elem->options, '' );
121161 }
122162 $html_options = array();
 163+ # prepare the list of selected values
 164+ if ( $elem->attributes['multiple'] !== null ) {
 165+ # new lines are separator for selected multiple options
 166+ $selected_values = explode( "\n", $elem->value );
 167+ } else {
 168+ $selected_values = array( $elem->value );
 169+ }
 170+ # generate options list
123171 foreach ( $elem->options as $option ) {
124172 $html_option = array(
125173 '__tag' => 'option',
126174 'value' => qp_Setup::entities( $option ),
127175 qp_Setup::specialchars( $option )
128176 );
129 - if ( $option === $elem->value ) {
 177+ if ( in_array( $option, $selected_values ) ) {
130178 $html_option['selected'] = 'selected';
131179 }
132180 $html_options[] = $html_option;
133181 }
134 - $this->cell[] = array(
 182+ $select = array(
135183 '__tag' => 'select',
136184 # unique (poll_type,order_id,question,proposal,category) "coordinate" for javascript
137185 'id' => "{$this->id_prefix}c{$this->ckey}",
@@ -138,9 +186,29 @@
139187 'name' => $elem->name,
140188 $html_options
141189 );
 190+ # multiple options 'name' attribute should have array hint []
 191+ if ( $elem->attributes['multiple'] !== null ) {
 192+ $select['multiple'] = 'multiple';
 193+ $select['name'] .= '[]';
 194+ }
 195+ # determine visual height of select options list
 196+ if ( ( $size = $elem->attributes['height'] ) !== 0 ) {
 197+ if ( is_int( $size ) ) {
 198+ if ( count( $elem->options ) < $size ) {
 199+ $size = count( $elem->options );
 200+ }
 201+ } else { /* 'auto' */
 202+ $size = count( $elem->options );
 203+ }
 204+ $select['size'] = $size;
 205+ }
 206+ $this->cell[] = $select;
142207 $this->ckey++;
143208 }
144209
 210+ /**
 211+ * Add tagarray representation of proposal part
 212+ */
145213 function addProposalPart( $elem ) {
146214 $this->cell[] = array(
147215 '__tag' => 'span',
@@ -149,6 +217,11 @@
150218 );
151219 }
152220
 221+ /**
 222+ * Build "final" cell which contain tagarray representation of
 223+ * proposal parts, proposal errors and one adjascent category
 224+ * and then add it to the row
 225+ */
153226 function addCell() {
154227 if ( count( $this->error ) > 0 ) {
155228 # merge previous errors to current cell
@@ -179,6 +252,11 @@
180253 # whether the resulting display table should be transposed
181254 # meaningful only when $this->tabularDisplay is true
182255 var $transposed = false;
 256+ # how many characters will hold horizontal line of textarea;
 257+ # currently is unused, because textarea 'cols' attribute renders
 258+ # poorly in table cells in modern versions of Firefox, so
 259+ # we are using CSS $this->textInputStyle instead
 260+ # var $textwidth = 0;
183261
184262 # default style of text input
185263 var $textInputStyle = '';
@@ -206,9 +284,11 @@
207285 $this->transposed = strpos( $layout, 'transpose' ) !== false;
208286 }
209287 if ( $textwidth !== null ) {
210 - $textwidth = intval( $textwidth );
211 - if ( $textwidth > 0 ) {
 288+ if ( is_numeric( $textwidth ) &&
 289+ $textwidth = intval( $textwidth ) > 0 ) {
212290 $this->textInputStyle = 'width:' . $textwidth . 'em;';
 291+ } elseif ( $textwidth === 'auto' ) {
 292+ $this->textInputStyle = 'width:auto;';
213293 }
214294 }
215295 }
@@ -290,9 +370,9 @@
291371 # create view for proposal/category error message
292372 $vr->addError( $elem );
293373 }
294 - # create view for the input options part
 374+ # create view for the input / textarea options part
295375 if ( count( $elem->options ) === 1 ) {
296 - # one option produces html text / radio / checkbox input
 376+ # one option produces html text / radio / checkbox input or an textarea
297377 $vr->addInput( $elem, $className );
298378 $vr->addCell();
299379 continue;

Comments

#Comment by Nikerabbit (talk | contribs)   14:58, 25 October 2011
+                       if ( is_numeric( $textwidth ) &&
+                                       $textwidth = intval( $textwidth ) > 0

Please move that assignment out of the if. The processing order in that is not immediately obvious.

#Comment by QuestPC (talk | contribs)   16:06, 25 October 2011

What if I'll put the assignment into the round braces, that should be good enough? I try to avoid unnecessarily "if" nesting, when possible.

+ if ( is_numeric( $textwidth ) && + ( $textwidth = intval( $textwidth ) ) > 0 ) {

There was a problem with assignment vs comparsion order. "Logical and" is always processed from the left to the right, even shell scripts assume that (I believe so).

Thank you for reviewing my code.

#Comment by QuestPC (talk | contribs)   16:08, 25 October 2011

Forgot to put the fragment into the pre

+                       if ( is_numeric( $textwidth ) &&
+                                       ( $textwidth = intval( $textwidth ) ) > 0 ) {
#Comment by Nikerabbit (talk | contribs)   16:08, 25 October 2011

It's a bit confusing in another way as well, think about "1e8".

> echo is_numeric( "1e8" );
1
> echo intval( "1e8" );
1
#Comment by QuestPC (talk | contribs)   17:34, 25 October 2011

I know that. However, PHP is slow language and extra checks for floating point numbers might negatively affect the performance. This does not produce any exploit, anyway. One might treat this not like floating point number but like separator.

I'll think about something like preg_match( '/^[1-9]\d?$/', $textwidth ), though. Probably should be as fast and not required to compare to zero.

#Comment by Nikerabbit (talk | contribs)   17:36, 25 October 2011

PHP is not *that* slow.

#Comment by QuestPC (talk | contribs)   06:46, 26 October 2011

I removed is_numeric() in r100796. Probably should be faster and cleaner with preg_match() - few comparsions and it would not pass insanely large integer numbers.

Status & tagging log