r100415 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r100414‎ | r100415 | r100416 >
Date:08:05, 21 October 2011
Author:questpc
Status:deferred
Tags:
Comment:
Refactoring of question type text view. Fixed some possible XSS. Impose length limits on text database fields.
Modified paths:
  • /trunk/extensions/QPoll/clientside/qp_user.js (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_poll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_textquestion.php (modified) (history)
  • /trunk/extensions/QPoll/i18n/qp.i18n.php (modified) (history)
  • /trunk/extensions/QPoll/interpretation/qp_interpret.php (modified) (history)
  • /trunk/extensions/QPoll/model/qp_pollstore.php (modified) (history)
  • /trunk/extensions/QPoll/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_textquestionview.php (modified) (history)

Diff [purge]

Index: trunk/extensions/QPoll/i18n/qp.i18n.php
@@ -108,6 +108,7 @@
109109 'qp_error_invalid_poll_id' => 'Invalid poll id (id=$1).
110110 Poll id may contain only letters, numbers and space character.',
111111 'qp_error_already_used_poll_id' => 'The poll id has already been used on this page (id=$1).',
 112+ 'qp_error_too_long_dependance_value' => 'The poll (id=$1) dependance attribute value (dependance="$2") is too long to be stored in database.',
112113 'qp_error_invalid_dependance_value' => 'The poll (id=$1) dependance chain has invalid value of dependance attribute (dependance="$2").',
113114 'qp_error_missed_dependance_title' => 'The poll (id=$1) is dependant on the another poll (id=$3) from page [[$2]], but the title [[$2]] was not found.
114115 Either remove the dependance attribute, or restore [[$2]].',
@@ -225,6 +226,9 @@
226227 'qp_error_invalid_question_type' => '{{Identical|Invalid value of qustion xml-like "type" attribute was specified. There is no such type of question. Please read the manual for list of valid question types.}}',
227228 'qp_error_type_in_stats_mode' => 'Question\'s "type" xml-like attribute is meaningless in statistical display mode.',
228229 'qp_error_no_poll_id' => 'Every poll definition in declaration / voting mode must have "id" attribute.',
 230+ 'qp_error_too_long_dependance_value' => 'Parameters:
 231+* $1 is the poll ID of the poll having an error.
 232+* $2 is the value of poll "dependance" attribute, which cannot be stored into database because it is too long.',
229233 'qp_error_missed_dependance_poll' => 'Parameters:
230234 * $1 is the poll ID of the poll having an error.
231235 * $2 is a link to the page with the poll, that this erroneous poll depends on.
@@ -2707,6 +2711,7 @@
27082712 'qp_error_no_poll_id' => 'Тэг опроса не имеет атрибута id.',
27092713 'qp_error_invalid_poll_id' => 'Недопустимый идентификатор опроса (id=$1). Идентификатор опроса может содержать только буквы, цифры и символ пробела',
27102714 'qp_error_already_used_poll_id' => 'Установленный атрибут id опроса уже используется другим опросом на данной странице (id=$1).',
 2715+ 'qp_error_too_long_dependance_value' => 'Значение атрибута зависимости опросов (dependance="$2") для опроса (id=$1) имеет слишком большую длину, из-за чего не может быть сохранено.',
27112716 'qp_error_invalid_dependance_value' => 'В цепочке зависимости опросов для опроса (id=$1) было найдено синтаксически неверное значение атрибута зависимости (dependance="$2")',
27122717 'qp_error_missed_dependance_title' => 'Опрос с идентификатором id=$1 имеет атрибут зависимости от другого опроса (id=$3), находящегося на отсутствующей странице [[$2]]. Необходимо убрать атрибут зависимости от другого опроса, либо восстановить страницу [[$2]]',
27132718 'qp_error_missed_dependance_poll' => 'Опрос с идентификатором id=$1 требует прохождения другого опроса с идентификатором id=$3, находящегося на странице $2. Однако же, последний не был найден. Необходимо удалить атрибут зависимости из опроса (id=$1), либо создать опрос с идентификатором id=$3 на странице $2 и сохранить его. Для сохранения опроса будет достаточно нажать кнопку "Проголосовать", не отвечая ни на один вопрос.',
Index: trunk/extensions/QPoll/clientside/qp_user.js
@@ -188,6 +188,7 @@
189189 case 'tx' :
190190 // handler for text question proposal rows
191191 addEvent( input[j], "click", self.clickTextRow );
 192+ break;
192193 }
193194 } else {
194195 // non-unique, non-mixed tabular questions
Index: trunk/extensions/QPoll/model/qp_pollstore.php
@@ -832,9 +832,10 @@
833833 }
834834
835835 private function setQuestionDesc() {
 836+ global $wgContLang;
836837 $insert = array();
837838 foreach ( $this->Questions as $qkey => &$ques ) {
838 - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $ques->CommonQuestion );
 839+ $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $wgContLang->truncate( $ques->CommonQuestion, qp_Setup::$field_max_len['common_question'] , '' ) );
839840 $ques->question_id = $qkey;
840841 }
841842 if ( count( $insert ) > 0 ) {
@@ -870,7 +871,7 @@
871872 if ( isset( $ques->ProposalNames[$propkey] ) ) {
872873 $ptext = qp_QuestionData::getProposalNamePrefix( $ques->ProposalNames[$propkey] ) . $ptext;
873874 }
874 - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$proposal_max_length , '' ) );
 875+ $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$field_max_len['proposal_text'] , '' ) );
875876 }
876877 }
877878 if ( count( $insert ) > 0 ) {
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php
@@ -162,15 +162,29 @@
163163 }
164164 }
165165
166 - static function fatalError() {
 166+ /**
 167+ * Warning! When calling, do not forget to htmlquote the arguments, when required!
 168+ */
 169+ static function fatalErrorNoQuote() {
167170 $args = func_get_args();
168 - $result = 'Extension bug: ' . __METHOD__ . ' called without arguments';
169 - if ( count( $args > 0 ) ) {
170 - $result = call_user_func_array( 'wfMsgHTML', $args );
 171+ if ( count( $args ) < 1 ) {
 172+ throw new MWException( 'Too few arguments in ' . __METHOD__ );
171173 }
172 - return '<div class="qpoll"><div class="fatalerror">' . $result . '</div></div>';
 174+ return '<div class="qpoll"><div class="fatalerror">' . call_user_func_array( 'wfMsgHTML', $args ) . '</div></div>';
173175 }
174176
 177+ static function fatalErrorQuote() {
 178+ $args = func_get_args();
 179+ if ( count( $args ) < 1 ) {
 180+ throw new MWException( 'Too few arguments in ' . __METHOD__ );
 181+ }
 182+ $key = array_shift( $args );
 183+ # quote the params (if any)
 184+ $args = array_map( array( 'qp_Setup', 'specialchars' ), $args );
 185+ array_unshift( $args, $key );
 186+ return call_user_func_array( array( self, 'fatalErrorNoQuote' ), $args );
 187+ }
 188+
175189 static function s_getPollTitleFragment( $pollid, $dash = '#' ) {
176190 return $dash . 'qp_' . Title::escapeFragmentForURL( $pollid );
177191 }
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php
@@ -55,11 +55,11 @@
5656 function setHeaders() {
5757 if ( $this->mPollId !== null ) {
5858 $this->mState = "error";
59 - return self::fatalError( 'qp_error_id_in_stats_mode' );
 59+ return self::fatalErrorNoQuote( 'qp_error_id_in_stats_mode' );
6060 }
6161 if ( isset( $this->dependsOn ) && $this->dependsOn !== '' ) {
6262 $this->mState = "error";
63 - return self::fatalError( 'qp_error_dependance_in_stats_mode' );
 63+ return self::fatalErrorNoQuote( 'qp_error_dependance_in_stats_mode' );
6464 }
6565 return true;
6666 }
@@ -72,11 +72,11 @@
7373 function getPollStore() {
7474 $this->pollStore = qp_PollStore::newFromAddr( $this->pollAddr );
7575 if ( !( $this->pollStore instanceof qp_PollStore ) || $this->pollStore->pid === null ) {
76 - return self::fatalError( 'qp_error_no_such_poll', $this->pollAddr );
 76+ return self::fatalErrorQuote( 'qp_error_no_such_poll', $this->pollAddr );
7777 }
7878 if ( !$this->pollStore->loadQuestions() ) {
7979 $this->mState = "error";
80 - return self::fatalError( 'qp_error_no_stats', $this->pollAddr );
 80+ return self::fatalErrorQuote( 'qp_error_no_stats', $this->pollAddr );
8181 }
8282 $this->pollStore->setLastUser( $this->username, false );
8383 # do not check the result, because we may show results even if the user hasn't voted
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php
@@ -108,25 +108,28 @@
109109 function setHeaders() {
110110 if ( $this->mPollId == null ) {
111111 $this->mState = "error";
112 - return self::fatalError( 'qp_error_no_poll_id' );
 112+ return self::fatalErrorNoQuote( 'qp_error_no_poll_id' );
113113 }
114114 if ( !self::isValidPollId( $this->mPollId ) ) {
115115 $this->mState = "error";
116 - return self::fatalError( 'qp_error_invalid_poll_id', $this->mPollId );
 116+ return self::fatalErrorQuote( 'qp_error_invalid_poll_id', $this->mPollId );
117117 }
118118 if ( !self::isUniquePollId( $this->mPollId ) ) {
119119 $this->mState = "error";
120 - return self::fatalError( 'qp_error_already_used_poll_id', $this->mPollId );
 120+ return self::fatalErrorQuote( 'qp_error_already_used_poll_id', $this->mPollId );
121121 }
122122 self::addPollId( $this->mPollId ); // add current poll id to the static list of poll ids on this page
123123 if ( $this->pollAddr !== null ) {
124124 $this->mState = "error";
125 - return self::fatalError( 'qp_error_address_in_decl_mode' );
 125+ return self::fatalErrorNoQuote( 'qp_error_address_in_decl_mode' );
126126 }
127127 if ( $this->dependsOn != '' ) {
 128+ if ( strlen( $this->dependsOn ) > qp_Setup::$field_max_len['dependance'] ) {
 129+ return self::fatalErrorQuote( 'qp_error_too_long_dependance_value', $this->mPollId, $this->dependsOn );
 130+ }
128131 $depsOnAddr = self::getPrefixedPollAddress( $this->dependsOn );
129132 if ( !is_array( $depsOnAddr ) ) {
130 - return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $this->dependsOn );
 133+ return self::fatalErrorQuote( 'qp_error_invalid_dependance_value', $this->mPollId, $this->dependsOn );
131134 }
132135 $this->dependsOn = $depsOnAddr[2];
133136 }
@@ -261,10 +264,10 @@
262265 $depPollId = $depPollStore->mPollId;
263266 $depLink = $this->view->link( $depTitle, $depTitle->getPrefixedText() . ' (' . $depPollStore->mPollId . ')' );
264267 if ( $depPollStore->pid === null ) {
265 - return self::fatalError( 'qp_error_missed_dependance_poll', $this->mPollId, $depLink, $depPollId );
 268+ return self::fatalErrorNoQuote( 'qp_error_missed_dependance_poll', qp_Setup::specialchars( $this->mPollId ), $depLink, qp_Setup::specialchars( $depPollId ) );
266269 }
267270 if ( !$depPollStore->loadQuestions() ) {
268 - return self::fatalError( 'qp_error_vote_dependance_poll', $depLink );
 271+ return self::fatalErrorNoQuote( 'qp_error_vote_dependance_poll', $depLink );
269272 }
270273 $depPollStore->setLastUser( $this->username, false );
271274 if ( $depPollStore->loadUserAlreadyVoted() ) {
@@ -275,7 +278,7 @@
276279 return true;
277280 } else {
278281 # there is an non-voted deplink in the chain at some previous level of recursion
279 - return self::fatalError( 'qp_error_vote_dependance_poll', $nonVotedDepLink );
 282+ return self::fatalErrorNoQuote( 'qp_error_vote_dependance_poll', $nonVotedDepLink );
280283 }
281284 } else {
282285 return $this->checkDependance( $depPollStore->dependsOn, $nonVotedDepLink );
@@ -284,7 +287,7 @@
285288 # user hasn't voted in current the poll in chain
286289 if ( $depPollStore->dependsOn === '' ) {
287290 # current element of chain is not voted and furthermore, doesn't depend on any other polls
288 - return self::fatalError( 'qp_error_vote_dependance_poll', $depLink );
 291+ return self::fatalErrorNoQuote( 'qp_error_vote_dependance_poll', $depLink );
289292 } else {
290293 # current element of chain is not voted, BUT it has it's own dependance
291294 # so we will check for the most deeply nested poll which hasn't voted, yet
@@ -295,7 +298,7 @@
296299 # process poll address errors
297300 switch ( $depPollStore ) {
298301 case qp_Setup::ERROR_INVALID_ADDRESS :
299 - return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn );
 302+ return self::fatalErrorQuote( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn );
300303 case qp_Setup::ERROR_MISSED_TITLE :
301304 $depSplit = self::getPrefixedPollAddress( $dependsOn );
302305 if ( is_array( $depSplit ) ) {
@@ -303,9 +306,9 @@
304307 $depTitle = Title::newFromURL( $depTitleStr );
305308 $depTitleStr = $depTitle->getPrefixedText();
306309 $depLink = $this->view->link( $depTitle, $depTitleStr );
307 - return self::fatalError( 'qp_error_missed_dependance_title', $this->mPollId, $depLink, $depPollId );
 310+ return self::fatalErrorNoQuote( 'qp_error_missed_dependance_title', qp_Setup::specialchars( $this->mPollId ), $depLink, qp_Setup::specialchars( $depPollId ) );
308311 } else {
309 - return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn );
 312+ return self::fatalErrorQuote( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn );
310313 }
311314 default :
312315 throw new MWException( __METHOD__ . ' invalid dependance poll store found' );
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php
@@ -159,7 +159,7 @@
160160 class qp_TextQuestion extends qp_StubQuestion {
161161
162162 # regexp for separation of proposal line tokens
163 - var $propCatPattern = null;
 163+ var $propCatPattern;
164164
165165 # source "raw" tokens (preg_split)
166166 var $rawtokens;
@@ -189,7 +189,11 @@
190190 # only proposal parts and category options
191191 var $dbtokens = array();
192192
193 - # list of opening input braces types
 193+ # List of opening input braces types
 194+ # input is html representation of category:
 195+ # brace value 'text' is mapped to input text or to select option;
 196+ # brace values 'radio' and 'checkbox' are mapped to
 197+ # input radio and input checkbox, respectively.
194198 var $input_braces_types = array(
195199 '<<' => 'text',
196200 '<(' => 'radio',
@@ -225,16 +229,14 @@
226230 unset( $this->matching_braces[$radio_brace] );
227231 }
228232 }
229 - if ( $this->propCatPattern === null ) {
230 - $braces_list = array_map( 'preg_quote',
231 - array_merge(
232 - ( array_values( $this->matching_braces ) ),
233 - array_keys( $this->matching_braces ),
234 - array( '|' )
235 - )
236 - );
237 - $this->propCatPattern = '/(' . implode( '|', $braces_list ) . ')/u';
238 - }
 233+ $braces_list = array_map( 'preg_quote',
 234+ array_merge(
 235+ ( array_values( $this->matching_braces ) ),
 236+ array_keys( $this->matching_braces ),
 237+ array( '|' )
 238+ )
 239+ );
 240+ $this->propCatPattern = '/(' . implode( '|', $braces_list ) . ')/u';
239241 }
240242
241243 /**
@@ -252,6 +254,7 @@
253255 * Also, stores checkbox / radio / text answer into the parsed tokens list (propview)
254256 */
255257 function loadProposalCategory( qp_TextQuestionOptions $opt, $proposalId, $catId ) {
 258+ global $wgContLang;
256259 $name = "q{$this->mQuestionId}p{$proposalId}s{$catId}";
257260 # default value for unanswered category
258261 # boolean true "checked" checkbox / radiobutton
@@ -261,8 +264,8 @@
262265 if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) !== null ) {
263266 if ( $opt->type === 'text' ) {
264267 if ( ( $ta = trim( $this->mRequest->getText( $name ) ) ) != '' ) {
265 - if ( strlen( $ta ) > qp_Setup::MAX_TEXT_ANSWER_LENGTH ) {
266 - $text_answer = substr( $ta, 0, qp_Setup::MAX_TEXT_ANSWER_LENGTH );
 268+ if ( strlen( $ta ) > qp_Setup::$field_max_len['text_answer'] ) {
 269+ $text_answer = $wgContLang->truncate( $ta, qp_Setup::$field_max_len['text_answer'] , '' );
267270 } else {
268271 $text_answer = $ta;
269272 }
@@ -358,8 +361,13 @@
359362 }
360363 }
361364 }
362 - # trying to backtrack non-closed braces only these which belong to
363 - # $this->input_braces_types
 365+ }
 366+
 367+ /**
 368+ * Trying to backtrack non-closed braces only for these which belong to
 369+ * $this->input_braces_types
 370+ */
 371+ private function backtrackMismatchingBraces() {
364372 $brace_keys = array_keys( $this->brace_matches, true );
365373 for ( $i = count( $brace_keys ) - 1; $i >= 0; $i-- ) {
366374 $brace_match = &$this->brace_matches[$brace_keys[$i]];
@@ -424,6 +432,7 @@
425433 $this->rawtokens = preg_split( $this->propCatPattern, $raw, -1, PREG_SPLIT_DELIM_CAPTURE );
426434 $matching_closed_brace = '';
427435 $this->findMatchingBraces();
 436+ $this->backtrackMismatchingBraces();
428437 foreach ( $this->rawtokens as $tkey => $token ) {
429438 # $toBeStored == true when current $token has to be stored into
430439 # category / proposal list (depending on $opt->isCatDef)
@@ -486,7 +495,7 @@
487496 }
488497 $proposal_text = serialize( $this->dbtokens );
489498 # build the whole raw DB proposal_text value to check it's maximal length
490 - if ( strlen( qp_QuestionData::getProposalNamePrefix( $prop_name ) . $proposal_text ) > qp_Setup::$proposal_max_length ) {
 499+ if ( strlen( qp_QuestionData::getProposalNamePrefix( $prop_name ) . $proposal_text ) > qp_Setup::$field_max_len['proposal_text'] ) {
491500 # too long proposal field to store into the DB
492501 # this is very important check for text questions because
493502 # category definitions are stored within the proposal text
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php
@@ -15,6 +15,7 @@
1616 * also may be altered during the poll generation
1717 */
1818 function parseBody() {
 19+ global $wgContLang;
1920 $this->mProposalPattern = '`^';
2021 foreach ( $this->mCategories as $catDesc ) {
2122 $this->mProposalPattern .= '(\[\]|\(\)|<>)';
@@ -76,8 +77,8 @@
7778 if ( $this->poll->mBeingCorrected && $this->mRequest->getVal( $name ) !== null ) {
7879 if ( $inputType == 'text' ) {
7980 $text_answer = trim( $this->mRequest->getText( $name ) );
80 - if ( strlen( $text_answer ) > qp_Setup::MAX_TEXT_ANSWER_LENGTH ) {
81 - $text_answer = substr( $text_answer, 0, qp_Setup::MAX_TEXT_ANSWER_LENGTH );
 81+ if ( strlen( $text_answer ) > qp_Setup::$field_max_len['text_answer'] ) {
 82+ $text_answer = $wgContLang->truncate( $text_answer, qp_Setup::$field_max_len['text_answer'] , '' );
8283 }
8384 if ( $text_answer != '' ) {
8485 $input_checked = true;
Index: trunk/extensions/QPoll/qp_user.php
@@ -143,7 +143,6 @@
144144
145145 const ERROR_MISSED_TITLE = 1;
146146 const ERROR_INVALID_ADDRESS = 2;
147 - const MAX_TEXT_ANSWER_LENGTH = 1024;
148147
149148 static $pollTag = 'qpoll';
150149 static $interpTag = 'qpinterpret';
@@ -157,6 +156,9 @@
158157 static $title; // Title instance we got from hook parameter
159158 static $user; // User instance we got from hook parameter
160159
 160+ /**
 161+ * The map of question 'type' attribute value to the question's ctrl / view / subtype.
 162+ */
161163 static $questionTypes = array(
162164 '[]' => array(
163165 'ctrl' => 'qp_TabularQuestion',
@@ -198,9 +200,11 @@
199201 NS_QP_INTERPRETATION_TALK => 'Interpretation_talk'
200202 );
201203
202 - # stores interpretation script line numbers separately for
203 - # every script language (currently only php eval is implemented)
204 - # key is value of <qpinterpret> xml tag 'lang' attribute, value is source line counter
 204+ /**
 205+ * Stores interpretation script line numbers separately for
 206+ * every script language.
 207+ * key is value of <qpinterpret> xml tag 'lang' attribute, value is source line counter
 208+ */
205209 static $scriptLinesCount = array();
206210
207211 /**
@@ -215,16 +219,26 @@
216220 public static $cache_control = false;
217221 # number of submit attempts allowed (0 or less for infinite number)
218222 public static $max_submit_attempts = 0;
219 - # maximal length of question proposal row
220 - # tuned for MySQL ROW_FORMAT=REDUNDANT, ROW_FORMAT=COMPACT
221 - # feel free to increase the value for ROW_FORMAT=DYNAMIC, ROW_FORMAT=COMPRESSED
222 - # however make sure that the whole row will fit into DB page,
223 - # otherwise the performance may decrease
224 - # it is important only for question type="text", where
225 - # proposal text contains serialized array of proposal parts and category fields
226 - public static $proposal_max_length = 768;
227 - # not larger than DB field size, otherwise serialization will be invalid
228 - public static $structured_interpretation_max_length = 65535;
 223+ /**
 224+ * Maximal length of table row is tuned for
 225+ * MySQL ROW_FORMAT=REDUNDANT, ROW_FORMAT=COMPACT .
 226+ * Feel free to increase the value for
 227+ * ROW_FORMAT=DYNAMIC, ROW_FORMAT=COMPRESSED ,
 228+ * however make sure that the whole row will fit into DB page.
 229+ * Otherwise the DB performance may decrease.
 230+ */
 231+ public static $field_max_len = array(
 232+ # not larger than DB field size, otherwise checking of dependance chain will fail:
 233+ 'dependance' => 768,
 234+ 'common_question' => 768,
 235+ # 'proposal_text' length is important for question type="text", where
 236+ # proposal text contains serialized array of proposal parts and category fields:
 237+ 'proposal_text' => 768,
 238+ 'text_answer' => 768,
 239+ 'long_interpretation' => 768,
 240+ # not larger than DB field size, otherwise unserialization will be invalid:
 241+ 'serialized_interpretation' => 65535
 242+ );
229243 # whether to show short, long, structured interpretation results to end user
230244 public static $show_interpretation = array(
231245 'error' => true,
@@ -235,11 +249,11 @@
236250 /* end of default configuration settings */
237251
238252 static function entities( $s ) {
239 - return htmlentities( $s, ENT_COMPAT, 'UTF-8' );
 253+ return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
240254 }
241255
242256 static function specialchars( $s ) {
243 - return htmlspecialchars( $s, ENT_COMPAT, 'UTF-8' );
 257+ return htmlentities( $s, ENT_QUOTES, 'UTF-8' );
244258 }
245259
246260 /**
Index: trunk/extensions/QPoll/interpretation/qp_interpret.php
@@ -69,7 +69,7 @@
7070 * @return instance of qp_InterpResult class (interpretation result)
7171 */
7272 static function getResult( $interpArticle, $injectVars ) {
73 - global $wgParser;
 73+ global $wgParser, $wgContLang;
7474 $matches = array();
7575 # extract <qpinterpret> tags from the article content
7676 $wgParser->extractTagsAndParams( array( qp_Setup::$interpTag ), $interpArticle->getRawText(), $matches );
@@ -153,12 +153,17 @@
154154 return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) );
155155 }
156156 $interpResult->structured = isset( $result['structured'] ) ? serialize( $result['structured'] ) : '';
157 - if ( strlen( $interpResult->structured ) > qp_Setup::$structured_interpretation_max_length ) {
 157+ if ( strlen( $interpResult->structured ) > qp_Setup::$field_max_len['serialized_interpretation'] ) {
 158+ # serialized structured interpretation is too long and
 159+ # this type of interpretation cannot be truncated
158160 unset( $interpResult->structured );
159161 return $interpResult->setError( wfMsg( 'qp_error_structured_interpretation_is_too_long' ) );
160162 }
161163 $interpResult->short = isset( $result['short'] ) ? strval( $result['short'] ) : '';
162164 $interpResult->long = isset( $result['long'] ) ? strval( $result['long'] ) : '';
 165+ if ( strlen( $interpResult->long ) > qp_Setup::$field_max_len['long_interpretation'] ) {
 166+ $interpResult->long = $wgContLang->truncate( $interpResult->long, qp_Setup::$field_max_len['long_interpretation'] , '' );
 167+ }
163168 return $interpResult;
164169 }
165170
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -43,6 +43,10 @@
4444 */
4545 class qp_TextQuestionViewRow {
4646
 47+ # owner of row ( an insance of qp_TextQuestionView )
 48+ var $owner;
 49+ # proposal prefix for category tag id generation
 50+ var $id_prefix;
4751 # each element of row is real table cell or "cell" with spans,
4852 # depending on $this->tabularDisplay value
4953 var $row;
@@ -51,17 +55,100 @@
5256 # tagarray with current cell builded for row
5357 # cell contains one or multiple tags, describing proposal part or category
5458 var $cell;
 59+ # current category id
 60+ var $ckey;
5561
56 - function __construct() {
57 - $this->reset();
 62+ function __construct( qp_TextQuestionView $owner ) {
 63+ $this->owner = $owner;
 64+ $this->reset( '' );
5865 }
5966
60 - function reset() {
 67+ function reset( $id_prefix ) {
 68+ $this->id_prefix = $id_prefix;
6169 $this->row = array();
6270 $this->error = array();
6371 $this->cell = array();
 72+ # category index, starting from 0
 73+ $this->ckey = 0;
6474 }
6575
 76+ function addError( $elem ) {
 77+ $this->cell[] = array(
 78+ '__tag' => 'span',
 79+ 'class' => 'proposalerror',
 80+ $elem->interpError
 81+ );
 82+ }
 83+
 84+ function addInput( $elem, $className ) {
 85+ $value = $elem->value;
 86+ # check, whether the definition of category has "pre-filled" value
 87+ # single, non-unanswered, non-empty option is a pre-filled value
 88+ if ( !$elem->unanswered && $elem->value === '' && $elem->options[0] !== '' ) {
 89+ # input text pre-fill
 90+ $value = $elem->options[0];
 91+ $className .= ' cat_prefilled';
 92+ }
 93+ $input = array(
 94+ '__tag' => 'input',
 95+ # unique (poll_type,order_id,question,proposal,category) "coordinate" for javascript
 96+ 'id' => "{$this->id_prefix}c{$this->ckey}",
 97+ 'class' => $className,
 98+ 'type' => $elem->type,
 99+ 'name' => $elem->name,
 100+ 'value' => qp_Setup::specialchars( $value )
 101+ );
 102+ $this->ckey++;
 103+ if ( $elem->type !== 'text' && $elem->attributes['checked'] === true ) {
 104+ $input['checked'] = 'checked';
 105+ }
 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;
 115+ }
 116+
 117+ function addSelect( $elem, $className ) {
 118+ if ( $elem->options[0] !== '' ) {
 119+ # default element in select/option set always must be empty option
 120+ array_unshift( $elem->options, '' );
 121+ }
 122+ $html_options = array();
 123+ foreach ( $elem->options as $option ) {
 124+ $html_option = array(
 125+ '__tag' => 'option',
 126+ 'value' => qp_Setup::entities( $option ),
 127+ qp_Setup::specialchars( $option )
 128+ );
 129+ if ( $option === $elem->value ) {
 130+ $html_option['selected'] = 'selected';
 131+ }
 132+ $html_options[] = $html_option;
 133+ }
 134+ $this->cell[] = array(
 135+ '__tag' => 'select',
 136+ # unique (poll_type,order_id,question,proposal,category) "coordinate" for javascript
 137+ 'id' => "{$this->id_prefix}c{$this->ckey}",
 138+ 'class' => $className,
 139+ 'name' => $elem->name,
 140+ $html_options
 141+ );
 142+ $this->ckey++;
 143+ }
 144+
 145+ function addProposalPart( $elem ) {
 146+ $this->cell[] = array(
 147+ '__tag' => 'span',
 148+ 'class' => 'prop_part',
 149+ $this->owner->rtp( $elem )
 150+ );
 151+ }
 152+
66153 function addCell() {
67154 if ( count( $this->error ) > 0 ) {
68155 # merge previous errors to current cell
@@ -105,7 +192,7 @@
106193 */
107194 function __construct( &$parser, &$frame, $showResults ) {
108195 parent::__construct( $parser, $frame );
109 - $this->vr = new qp_TextQuestionViewRow();
 196+ $this->vr = new qp_TextQuestionViewRow( $this );
110197 /* todo: implement showResults */
111198 }
112199
@@ -185,12 +272,9 @@
186273 * @return tagarray
187274 */
188275 function renderParsedProposal( $pkey, &$viewtokens ) {
189 - # proposal prefix for id generation
190 - $id_prefix = "tx{$this->ctrl->poll->mOrderId}q{$this->ctrl->mQuestionId}p{$pkey}";
191276 $vr = $this->vr;
192 - $vr->reset();
193 - # category index, starting from 0
194 - $ckey = 0;
 277+ # proposal prefix for category tag id generation
 278+ $vr->reset( "tx{$this->ctrl->poll->mOrderId}q{$this->ctrl->mQuestionId}p{$pkey}" );
195279 foreach ( $viewtokens as $elem ) {
196280 $vr->cell = array();
197281 if ( is_object( $elem ) ) {
@@ -204,74 +288,17 @@
205289 $className .= ' cat_noanswer';
206290 }
207291 # create view for proposal/category error message
208 - $vr->cell[] = array(
209 - '__tag' => 'span',
210 - 'class' => 'proposalerror',
211 - $elem->interpError
212 - );
 292+ $vr->addError( $elem );
213293 }
214294 # create view for the input options part
215295 if ( count( $elem->options ) === 1 ) {
216296 # one option produces html text / radio / checkbox input
217 - $value = $elem->value;
218 - # check, whether the definition of category has "pre-filled" value
219 - # single, non-unanswered, non-empty option is a pre-filled value
220 - if ( !$elem->unanswered && $elem->value === '' && $elem->options[0] !== '' ) {
221 - # input text pre-fill
222 - $value = $elem->options[0];
223 - $className .= ' cat_prefilled';
224 - }
225 - $input = array(
226 - '__tag' => 'input',
227 - # unique (orderid,question,proposal,category) "coordinate" for javascript
228 - 'id' => "{$id_prefix}c{$ckey}",
229 - 'class' => $className,
230 - 'type' => $elem->type,
231 - 'name' => $elem->name,
232 - 'value' => qp_Setup::specialchars( $value )
233 - );
234 - $ckey++;
235 - if ( $elem->type !== 'text' && $elem->attributes['checked'] === true ) {
236 - $input['checked'] = 'checked';
237 - }
238 - if ( $this->textInputStyle != '' ) {
239 - # apply poll's textwidth attribute
240 - $input['style'] = $this->textInputStyle;
241 - }
242 - if ( $elem->attributes['width'] !== null ) {
243 - # apply current category width attribute
244 - $input['style'] = 'width:' . intval( $elem->attributes['width'] ) . 'em;';
245 - }
246 - $vr->cell[] = $input;
 297+ $vr->addInput( $elem, $className );
247298 $vr->addCell();
248299 continue;
249300 }
250301 # multiple options produce html select / options
251 - if ( $elem->options[0] !== '' ) {
252 - # default element in select/option set always must be empty option
253 - array_unshift( $elem->options, '' );
254 - }
255 - $html_options = array();
256 - foreach ( $elem->options as $option ) {
257 - $html_option = array(
258 - '__tag' => 'option',
259 - 'value' => qp_Setup::entities( $option ),
260 - qp_Setup::specialchars( $option )
261 - );
262 - if ( $option === $elem->value ) {
263 - $html_option['selected'] = 'selected';
264 - }
265 - $html_options[] = $html_option;
266 - }
267 - $vr->cell[] = array(
268 - '__tag' => 'select',
269 - # unique (poll,question,proposal,category) "coordinate" for javascript
270 - 'id' => "{$id_prefix}c{$ckey}",
271 - 'class' => $className,
272 - 'name' => $elem->name,
273 - $html_options
274 - );
275 - $ckey++;
 302+ $vr->addSelect( $elem, $className );
276303 $vr->addCell();
277304 } elseif ( isset( $elem->error ) ) {
278305 # create view for proposal/category error message
@@ -285,11 +312,7 @@
286313 }
287314 } else {
288315 # create view for the proposal part
289 - $vr->cell[] = array(
290 - '__tag' => 'span',
291 - 'class' => 'prop_part',
292 - $this->rtp( $elem )
293 - );
 316+ $vr->addProposalPart( $elem );
294317 $vr->addCell();
295318 }
296319 }

Status & tagging log