Index: trunk/extensions/QPoll/qp_questiondata.php |
— | — | @@ -1,273 +0,0 @@ |
2 | | -<?php |
3 | | - |
4 | | -if ( !defined( 'MEDIAWIKI' ) ) { |
5 | | - die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
6 | | -} |
7 | | - |
8 | | -/** |
9 | | - * Poll's single question data object RAM storage |
10 | | - * ( instances usually have short name qdata ) |
11 | | - * |
12 | | - * Since v0.8.0 it is also used as "mini-view" for Special:Pollresults page |
13 | | - * because different types of questions should be displayed differently |
14 | | - * |
15 | | - */ |
16 | | -class qp_QuestionData { |
17 | | - |
18 | | - // DB index (with current scheme is non-unique) |
19 | | - var $question_id = null; |
20 | | - // common properties |
21 | | - var $type; |
22 | | - var $CommonQuestion; |
23 | | - var $Categories; |
24 | | - var $CategorySpans; |
25 | | - var $ProposalText; |
26 | | - var $ProposalCategoryId; |
27 | | - var $ProposalCategoryText; |
28 | | - var $alreadyVoted = false; // whether the selected user already voted this question ? |
29 | | - // statistics storage |
30 | | - var $Votes = null; |
31 | | - var $Percents = null; |
32 | | - |
33 | | - /** |
34 | | - * Please do not instantiate directly, use qp_PollStore::newQuestionData() instead |
35 | | - */ |
36 | | - function __construct( $argv ) { |
37 | | - if ( array_key_exists( 'from', $argv ) ) { |
38 | | - switch ( $argv[ 'from' ] ) { |
39 | | - case 'postdata' : |
40 | | - $this->type = $argv[ 'type' ]; |
41 | | - $this->CommonQuestion = $argv[ 'common_question' ]; |
42 | | - $this->Categories = $argv[ 'categories' ]; |
43 | | - $this->CategorySpans = $argv[ 'category_spans' ]; |
44 | | - $this->ProposalText = $argv[ 'proposal_text' ]; |
45 | | - $this->ProposalCategoryId = $argv[ 'proposal_category_id' ]; |
46 | | - $this->ProposalCategoryText = $argv[ 'proposal_category_text' ]; |
47 | | - break; |
48 | | - case 'qid' : |
49 | | - $this->question_id = $argv[ 'qid' ]; |
50 | | - $this->type = $argv[ 'type' ]; |
51 | | - $this->CommonQuestion = $argv[ 'common_question' ]; |
52 | | - $this->Categories = Array(); |
53 | | - $this->CategorySpans = Array(); |
54 | | - $this->ProposalText = Array(); |
55 | | - $this->ProposalCategoryId = Array(); |
56 | | - $this->ProposalCategoryText = Array(); |
57 | | - break; |
58 | | - } |
59 | | - } |
60 | | - } |
61 | | - |
62 | | - // integrate spans into categories |
63 | | - function packSpans() { |
64 | | - if ( count( $this->CategorySpans ) > 0 ) { |
65 | | - foreach ( $this->Categories as &$Cat ) { |
66 | | - if ( array_key_exists( 'spanId', $Cat ) ) { |
67 | | - $Cat['name'] = $this->CategorySpans[ $Cat['spanId'] ]['name'] . "\n" . $Cat['name']; |
68 | | - unset( $Cat['spanId'] ); |
69 | | - } |
70 | | - } |
71 | | - unset( $this->CategorySpans ); |
72 | | - $this->CategorySpans = Array(); |
73 | | - } |
74 | | - } |
75 | | - |
76 | | - // restore spans from categories |
77 | | - function restoreSpans() { |
78 | | - if ( count( $this->CategorySpans ) == 0 ) { |
79 | | - $prevSpanName = ''; |
80 | | - $spanId = -1; |
81 | | - foreach ( $this->Categories as &$Cat ) { |
82 | | - $a = explode( "\n", $Cat['name'] ); |
83 | | - if ( count( $a ) > 1 ) { |
84 | | - if ( $prevSpanName != $a[0] ) { |
85 | | - $spanId++; |
86 | | - $prevSpanName = $a[0]; |
87 | | - $this->CategorySpans[ $spanId ]['count'] = 0; |
88 | | - } |
89 | | - $Cat['name'] = $a[1]; |
90 | | - $Cat['spanId'] = $spanId; |
91 | | - $this->CategorySpans[ $spanId ]['name'] = $a[0]; |
92 | | - $this->CategorySpans[ $spanId ]['count']++; |
93 | | - } else { |
94 | | - $prevSpanName = ''; |
95 | | - } |
96 | | - } |
97 | | - } |
98 | | - } |
99 | | - |
100 | | - // check whether the previousely stored poll header is compatible with the one defined on the page |
101 | | - // used to reject previous vote in case the header is incompatble |
102 | | - function isCompatible( &$question ) { |
103 | | - if ( $question->mType != $this->type ) { |
104 | | - return false; |
105 | | - } |
106 | | - if ( count( $question->mCategorySpans ) != count( $this->CategorySpans ) ) { |
107 | | - return false; |
108 | | - } |
109 | | - foreach ( $question->mCategorySpans as $spanidx => &$span ) { |
110 | | - if ( !isset( $this->CategorySpans[ $spanidx ] ) || |
111 | | - $span[ "count" ] != $this->CategorySpans[ $spanidx ][ "count" ] ) { |
112 | | - return false; |
113 | | - } |
114 | | - } |
115 | | - return true; |
116 | | - } |
117 | | - |
118 | | - private function categoryentities( $cat ) { |
119 | | - $cat['name'] = qp_Setup::entities( $cat['name'] ); |
120 | | - return $cat; |
121 | | - } |
122 | | - |
123 | | - /** |
124 | | - * @return string html representation of user vote for Special:Pollresults output |
125 | | - */ |
126 | | - function displayUserQuestionVote() { |
127 | | - $output = "<div class=\"qpoll\">\n" . "<table class=\"pollresults\">\n"; |
128 | | - $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $this->CategorySpans ), array( 'class' => 'spans' ), 'th', array( 'count' => 'colspan', 'name' => 0 ) ); |
129 | | - $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $this->Categories ), '', 'th', array( 'name' => 0 ) ); |
130 | | - # multiple choice polls doesn't use real spans, instead, every column is like "span" |
131 | | - $spansUsed = count( $this->CategorySpans ) > 0 || $this->type == "multipleChoice"; |
132 | | - foreach ( $this->ProposalText as $propkey => &$proposal_text ) { |
133 | | - $row = Array(); |
134 | | - foreach ( $this->Categories as $catkey => &$cat_name ) { |
135 | | - $cell = Array( 0 => "" ); |
136 | | - if ( array_key_exists( $propkey, $this->ProposalCategoryId ) && |
137 | | - ( $id_key = array_search( $catkey, $this->ProposalCategoryId[ $propkey ] ) ) !== false ) { |
138 | | - $text_answer = $this->ProposalCategoryText[ $propkey ][ $id_key ]; |
139 | | - if ( $text_answer != '' ) { |
140 | | - if ( strlen( $text_answer ) > 20 ) { |
141 | | - $cell[ 0 ] = array( '__tag' => 'div', 'style' => 'width:10em; height:5em; overflow:auto', 0 => qp_Setup::entities( $text_answer ) ); |
142 | | - } else { |
143 | | - $cell[ 0 ] = qp_Setup::entities( $text_answer ); |
144 | | - } |
145 | | - } else { |
146 | | - $cell[ 0 ] = '+'; |
147 | | - } |
148 | | - } |
149 | | - if ( $spansUsed ) { |
150 | | - if ( $this->type == "multipleChoice" ) { |
151 | | - $cell[ "class" ] = ( ( $catkey & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
152 | | - } else { |
153 | | - $cell[ "class" ] = ( ( $this->Categories[ $catkey ][ "spanId" ] & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
154 | | - } |
155 | | - } else { |
156 | | - $cell[ "class" ] = "stats"; |
157 | | - } |
158 | | - $row[] = $cell; |
159 | | - } |
160 | | - $row[] = array( 0 => qp_Setup::entities( $proposal_text ), "style" => "text-align:left;" ); |
161 | | - $output .= qp_Renderer::displayRow( $row ); |
162 | | - } |
163 | | - $output .= "</table>\n" . "</div>\n"; |
164 | | - return $output; |
165 | | - } |
166 | | - |
167 | | - /** |
168 | | - * @return string html representation of question statistics for Special:Pollresults output |
169 | | - */ |
170 | | - function displayQuestionStats( qp_SpecialPage $page, $pid ) { |
171 | | - $current_title = $page->getTitle(); |
172 | | - $output = "<br />\n<b>" . $this->question_id . ".</b> " . qp_Setup::entities( $this->CommonQuestion ) . "<br />\n"; |
173 | | - $output .= "<div class=\"qpoll\">\n" . "<table class=\"pollresults\">\n"; |
174 | | - $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $this->CategorySpans ), array( 'class' => 'spans' ), 'th', array( 'count' => 'colspan', 'name' => 0 ) ); |
175 | | - $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $this->Categories ), '', 'th', array( 'name' => 0 ) ); |
176 | | - # multiple choice polls doesn't use real spans, instead, every column is like "span" |
177 | | - $spansUsed = count( $this->CategorySpans ) > 0 || $this->type == "multipleChoice"; |
178 | | - foreach ( $this->ProposalText as $propkey => &$proposal_text ) { |
179 | | - if ( isset( $this->Votes[ $propkey ] ) ) { |
180 | | - if ( $this->Percents === null ) { |
181 | | - $row = $this->Votes[ $propkey ]; |
182 | | - } else { |
183 | | - $row = $this->Percents[ $propkey ]; |
184 | | - foreach ( $row as $catkey => &$cell ) { |
185 | | - # Replace spaces with en spaces |
186 | | - $formatted_cell = str_replace( " ", " ", sprintf( '%3d%%', intval( round( 100 * $cell ) ) ) ); |
187 | | - # only percents !=0 are displayed as link |
188 | | - if ( $cell == 0.0 && $this->question_id !== null ) { |
189 | | - $cell = array( 0 => $formatted_cell, "style" => "color:gray" ); |
190 | | - } else { |
191 | | - $cell = array( 0 => $page->qpLink( $current_title, $formatted_cell, |
192 | | - array( "title" => wfMsgExt( 'qp_votes_count', array( 'parsemag' ), $this->Votes[ $propkey ][ $catkey ] ) ), |
193 | | - array( "action" => "qpcusers", "id" => $pid, "qid" => $this->question_id, "pid" => $propkey, "cid" => $catkey ) ) ); |
194 | | - } |
195 | | - if ( $spansUsed ) { |
196 | | - if ( $this->type == "multipleChoice" ) { |
197 | | - $cell[ "class" ] = ( ( $catkey & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
198 | | - } else { |
199 | | - $cell[ "class" ] = ( ( $this->Categories[ $catkey ][ "spanId" ] & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
200 | | - } |
201 | | - } else { |
202 | | - $cell[ "class" ] = "stats"; |
203 | | - } |
204 | | - } |
205 | | - } |
206 | | - } else { |
207 | | - # this proposal has no statistics (no votes) |
208 | | - $row = array_fill( 0, count( $this->Categories ), '' ); |
209 | | - } |
210 | | - $row[] = array( 0 => qp_Setup::entities( $proposal_text ), "style" => "text-align:left;" ); |
211 | | - $output .= qp_Renderer::displayRow( $row ); |
212 | | - } |
213 | | - $output .= "</table>\n" . "</div>\n"; |
214 | | - return $output; |
215 | | - } |
216 | | - |
217 | | -} /* end of qp_QuestionData class */ |
218 | | - |
219 | | -/** |
220 | | - * Questions of type="text" require a different view logic at the Special:Pollresults page |
221 | | - */ |
222 | | -class qp_TextQuestionData extends qp_QuestionData { |
223 | | - |
224 | | -/** |
225 | | - * Please do not instantiate directly, use qp_PollStore::newQuestionData() instead |
226 | | - */ |
227 | | - |
228 | | - /** |
229 | | - * @return string html representation of user vote for Special:Pollresults output |
230 | | - */ |
231 | | - function displayUserQuestionVote() { |
232 | | - $output = "<div class=\"qpoll\">\n" . "<table class=\"pollresults\">\n"; |
233 | | - foreach ( $this->ProposalText as $propkey => &$serialized_tokens ) { |
234 | | - if ( !is_array( $dbtokens = unserialize( $serialized_tokens ) ) ) { |
235 | | - throw new MWException( 'dbtokens is not an array in ' . __METHOD__ ); |
236 | | - } |
237 | | - $catId = 0; |
238 | | - $row = array(); |
239 | | - foreach ( $dbtokens as &$token ) { |
240 | | - if ( is_string( $token ) ) { |
241 | | - # add a proposal part |
242 | | - $row[] = array( '__tag' => 'span', 'class' => 'prop_part', qp_Setup::entities( $token ) ); |
243 | | - } elseif ( is_array( $token ) ) { |
244 | | - # add a category definition with selected text answer (if any) |
245 | | - if ( array_key_exists( $propkey, $this->ProposalCategoryId ) && |
246 | | - ( $id_key = array_search( $catId, $this->ProposalCategoryId[$propkey] ) ) !== false ) { |
247 | | - $text_answer = $this->ProposalCategoryText[$propkey][$id_key]; |
248 | | - } else { |
249 | | - $text_answer = ''; |
250 | | - } |
251 | | - $className = ( count( $token ) === 1 || in_array( $text_answer, $token ) ) ? 'cat_part' : 'cat_unknown'; |
252 | | - $titleAttr = ''; |
253 | | - foreach ( $token as &$option ) { |
254 | | - if ( $option !== $text_answer ) { |
255 | | - if ( $titleAttr !== '' ) { |
256 | | - $titleAttr .= ' | '; |
257 | | - } |
258 | | - $titleAttr .= qp_Setup::entities( $option ); |
259 | | - } |
260 | | - } |
261 | | - $row[] = array( '__tag' => 'span', 'class' => $className, 'title'=>$titleAttr, qp_Setup::entities( $text_answer ) ); |
262 | | - # move to the next category (if any) |
263 | | - $catId++; |
264 | | - } else { |
265 | | - throw new MWException( 'DB token has invalid type (' . gettype( $token ) . ') in ' . __METHOD__ ); |
266 | | - } |
267 | | - } |
268 | | - $output .= qp_Renderer::displayRow( array( $row ) ); |
269 | | - } |
270 | | - $output .= "</table>\n" . "</div>\n"; |
271 | | - return $output; |
272 | | - } |
273 | | - |
274 | | -} /* end of qp_TextQuestionData class */ |
Index: trunk/extensions/QPoll/qp_interpret.php |
— | — | @@ -1,159 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * ***** BEGIN LICENSE BLOCK ***** |
5 | | - * This file is part of QPoll. |
6 | | - * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
7 | | - * |
8 | | - * QPoll is free software; you can redistribute it and/or modify |
9 | | - * it under the terms of the GNU General Public License as published by |
10 | | - * the Free Software Foundation; either version 2 of the License, or |
11 | | - * (at your option) any later version. |
12 | | - * |
13 | | - * QPoll is distributed in the hope that it will be useful, |
14 | | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | | - * GNU General Public License for more details. |
17 | | - * |
18 | | - * You should have received a copy of the GNU General Public License |
19 | | - * along with QPoll; if not, write to the Free Software |
20 | | - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
21 | | - * |
22 | | - * ***** END LICENSE BLOCK ***** |
23 | | - * |
24 | | - * QPoll is a poll tool for MediaWiki. |
25 | | - * |
26 | | - * To activate this extension : |
27 | | - * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
28 | | - * * Place the files from the extension archive there. |
29 | | - * * Add this line at the end of your LocalSettings.php file : |
30 | | - * require_once "$IP/extensions/QPoll/qp_user.php"; |
31 | | - * |
32 | | - * @version 0.8.0a |
33 | | - * @link http://www.mediawiki.org/wiki/Extension:QPoll |
34 | | - * @author QuestPC <questpc@rambler.ru> |
35 | | - */ |
36 | | - |
37 | | -if ( !defined( 'MEDIAWIKI' ) ) { |
38 | | - die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
39 | | -} |
40 | | - |
41 | | -class qp_Interpret { |
42 | | - |
43 | | - /** |
44 | | - * Lint the code of specified language by appropriate interpretator |
45 | | - * @param $lang string language key (eg. 'php') |
46 | | - * @param $code string source code |
47 | | - * @return bool true, when code has no syntax errors; |
48 | | - * string error message from lint |
49 | | - */ |
50 | | - static function lint( $lang, $code ) { |
51 | | - switch ( $lang ) { |
52 | | - case 'php' : |
53 | | - return qp_Eval::lint( $code ); |
54 | | - default : |
55 | | - # unknown languages syntax is "valid" because it cannot be checked |
56 | | - return true; |
57 | | - } |
58 | | - } |
59 | | - |
60 | | - /** |
61 | | - * Glues the content of <qpinterpret> tags together, checks "lang" attribute |
62 | | - * and calls appropriate interpretator to evaluate the user answer |
63 | | - * |
64 | | - * @param $interpArticle _existing_ Article with interpretation script enclosed in <qpinterp> tags |
65 | | - * @param $injectVars array with the following possible keys: |
66 | | - * key 'answer' array of user selected categories for |
67 | | - * every proposal & question of the poll; |
68 | | - * key 'usedQuestions' array of used questions for randomized polls |
69 | | - * or false, when the poll questions were not randomized |
70 | | - * @return instance of qp_InterpResult class (interpretation result) |
71 | | - */ |
72 | | - static function getResult( $interpArticle, $injectVars ) { |
73 | | - global $wgParser; |
74 | | - $matches = array(); |
75 | | - # extract <qpinterpret> tags from the article content |
76 | | - $wgParser->extractTagsAndParams( array( qp_Setup::$interpTag ), $interpArticle->getRawText(), $matches ); |
77 | | - $interpResult = new qp_InterpResult(); |
78 | | - # glue content of all <qpinterpret> tags at the page together |
79 | | - $interpretScript = ''; |
80 | | - $lang = ''; |
81 | | - foreach ( $matches as &$match ) { |
82 | | - list( $tagName, $content, $attrs ) = $match; |
83 | | - # basic checks for lang attribute (only lang="php" is implemented yet) |
84 | | - # however we do not want to limit interpretation language, |
85 | | - # so the attribute is enforced to use |
86 | | - if ( !isset( $attrs['lang'] ) ) { |
87 | | - return $interpResult->setError( wfMsg( 'qp_error_eval_missed_lang_attr' ) ); |
88 | | - } |
89 | | - if ( $lang == '' ) { |
90 | | - $lang = $attrs['lang']; |
91 | | - } elseif ( $attrs['lang'] != $lang ) { |
92 | | - return $interpResult->setError( wfMsg( 'qp_error_eval_mix_languages', $lang, $attrs['lang'] ) ); |
93 | | - } |
94 | | - if ( $tagName == qp_Setup::$interpTag ) { |
95 | | - $interpretScript .= $content; |
96 | | - } |
97 | | - } |
98 | | - switch ( $lang ) { |
99 | | - case 'php' : |
100 | | - $result = qp_Eval::interpretAnswer( $interpretScript, $injectVars, $interpResult ); |
101 | | - if ( $result instanceof qp_InterpResult ) { |
102 | | - # evaluation error (environment error) , return it; |
103 | | - return $interpResult; |
104 | | - } |
105 | | - break; |
106 | | - default : |
107 | | - return $interpResult->setError( wfMsg( 'qp_error_eval_unsupported_language', $lang ) ); |
108 | | - } |
109 | | - /*** process the result ***/ |
110 | | - if ( !is_array( $result ) ) { |
111 | | - return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
112 | | - } |
113 | | - # initialize $interpResult->qpcErrors[] member array |
114 | | - foreach ( $result as $qidx => $question ) { |
115 | | - if ( is_int( $qidx ) && is_array( $question ) ) { |
116 | | - foreach ( $question as $pidx => $prop_error ) { |
117 | | - if ( is_int( $pidx ) ) { |
118 | | - if ( is_array( $prop_error ) ) { |
119 | | - # separate error messages list for proposal categories |
120 | | - foreach ( $prop_error as $cidx => $cat_error ) { |
121 | | - if ( is_int( $cidx ) ) { |
122 | | - $interpResult->setQPCerror( $cat_error, $qidx, $pidx, $cidx ); |
123 | | - } |
124 | | - } |
125 | | - } else { |
126 | | - # error message for the whole proposal line |
127 | | - $interpResult->setQPCerror( $prop_error, $qidx, $pidx ); |
128 | | - } |
129 | | - } |
130 | | - } |
131 | | - } |
132 | | - } |
133 | | - if ( isset( $result['error'] ) && trim( $result['error'] ) != '' ) { |
134 | | - # script-generated error for the whole answer |
135 | | - return $interpResult->setError( (string) $result['error'] ); |
136 | | - } |
137 | | - # if there were question/proposal errors, return them; |
138 | | - if ( $interpResult->isError() ) { |
139 | | - return $interpResult->setDefaultErrorMessage(); |
140 | | - } |
141 | | - $interpCount = 0; |
142 | | - foreach ( qp_Setup::$show_interpretation as $interpType => $show ) { |
143 | | - if ( isset( $result[$interpType] ) ) { |
144 | | - $interpCount++; |
145 | | - } |
146 | | - } |
147 | | - if ( $interpCount == 0 ) { |
148 | | - return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
149 | | - } |
150 | | - $interpResult->structured = serialize( isset( $result['structured'] ) ? $result['structured'] : null ); |
151 | | - if ( strlen( $interpResult->structured ) > qp_Setup::$structured_interpretation_max_length ) { |
152 | | - unset( $interpResult->structured ); |
153 | | - return $interpResult->setError( wfMsg( 'qp_error_structured_interpretation_is_too_long' ) ); |
154 | | - } |
155 | | - $interpResult->short = isset( $result['short'] ) ? strval( $result['short'] ) : ''; |
156 | | - $interpResult->long = isset( $result['long'] ) ? strval( $result['long'] ) : ''; |
157 | | - return $interpResult; |
158 | | - } |
159 | | - |
160 | | -} /* end of qp_Interpret class */ |
Index: trunk/extensions/QPoll/qp_question_collection.php |
— | — | @@ -1,192 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * ***** BEGIN LICENSE BLOCK ***** |
5 | | - * This file is part of QPoll. |
6 | | - * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
7 | | - * |
8 | | - * QPoll is free software; you can redistribute it and/or modify |
9 | | - * it under the terms of the GNU General Public License as published by |
10 | | - * the Free Software Foundation; either version 2 of the License, or |
11 | | - * (at your option) any later version. |
12 | | - * |
13 | | - * QPoll is distributed in the hope that it will be useful, |
14 | | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | | - * GNU General Public License for more details. |
17 | | - * |
18 | | - * You should have received a copy of the GNU General Public License |
19 | | - * along with QPoll; if not, write to the Free Software |
20 | | - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
21 | | - * |
22 | | - * ***** END LICENSE BLOCK ***** |
23 | | - * |
24 | | - * QPoll is a poll tool for MediaWiki. |
25 | | - * |
26 | | - * To activate this extension : |
27 | | - * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
28 | | - * * Place the files from the extension archive there. |
29 | | - * * Add this line at the end of your LocalSettings.php file : |
30 | | - * require_once "$IP/extensions/QPoll/qp_user.php"; |
31 | | - * |
32 | | - * @version 0.8.0a |
33 | | - * @link http://www.mediawiki.org/wiki/Extension:QPoll |
34 | | - * @author QuestPC <questpc@rambler.ru> |
35 | | - */ |
36 | | - |
37 | | -if ( !defined( 'MEDIAWIKI' ) ) { |
38 | | - die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
39 | | -} |
40 | | - |
41 | | -/** |
42 | | - * Contains the iterable collection of questions with possible randomization |
43 | | - * (optional selection of some random questions from the whole set) |
44 | | - */ |
45 | | -class qp_QuestionCollection { |
46 | | - |
47 | | - /** |
48 | | - * Note: |
49 | | - * We assume that $questions and $usedQuestions do not have sparce keys |
50 | | - * I was using internal indexes but each() was buggy and evil even in PHP 5.3.x |
51 | | - */ |
52 | | - |
53 | | - # array of question objects associated with current poll |
54 | | - private $questions = array(); |
55 | | - # current questions key, starting from 1 |
56 | | - private $qKey; |
57 | | - # array of $this->questions[] indexes for question iterator (used by randomizer) |
58 | | - private $usedQuestions = false; |
59 | | - # current usedQuestions key, starting from 0 |
60 | | - private $usedKey; |
61 | | - |
62 | | - /** |
63 | | - * From http://php.net/manual/en/function.mt-rand.php |
64 | | - * function returns a random integer between min and max, just like function rand() does. |
65 | | - * Difference to rand is that the random generated number will not use any of the values |
66 | | - * placed in $except. ($except must therefore be an array) |
67 | | - * function returns false if $except holds all values between $min and $max. |
68 | | - */ |
69 | | - function rand_except( $min, $max, $except ) { |
70 | | - # first sort array values |
71 | | - sort( $except, SORT_NUMERIC ); |
72 | | - # calculate average gap between except-values |
73 | | - $except_count = count( $except ); |
74 | | - $avg_gap = ( $max - $min + 1 - $except_count ) / ( $except_count + 1 ); |
75 | | - if ( $avg_gap <= 0 ) { |
76 | | - return false; |
77 | | - } |
78 | | - # now add min and max to $except, so all gaps between $except-values can be calculated |
79 | | - array_unshift( $except, $min - 1 ); |
80 | | - array_push( $except, $max + 1 ); |
81 | | - $except_count += 2; |
82 | | - # iterate through all values of except. If gap between 2 values is higher than average gap, |
83 | | - # create random in this gap |
84 | | - for ( $i = 1; $i < $except_count; $i++ ) { |
85 | | - if ( $except[$i] - $except[$i - 1] - 1 >= $avg_gap ) { |
86 | | - return mt_rand( $except[$i - 1] + 1, $except[$i] - 1 ); |
87 | | - } |
88 | | - } |
89 | | - return false; |
90 | | - } |
91 | | - |
92 | | - function randomize( $randomQuestionCount ) { |
93 | | - $questionCount = count( $this->questions ); |
94 | | - if ( $randomQuestionCount > $questionCount ) { |
95 | | - $randomQuestionCount = $questionCount; |
96 | | - } |
97 | | - $this->usedQuestions = array(); |
98 | | - for ( $i = 0; $i < $randomQuestionCount; $i++ ) { |
99 | | - if ( ( $r = $this->rand_except( 1, $questionCount, $this->usedQuestions ) ) === false ) { |
100 | | - throw new MWException( 'Bug: too many random questions in ' . __METHOD__ ); |
101 | | - } |
102 | | - $this->usedQuestions[] = $r; |
103 | | - } |
104 | | - sort( $this->usedQuestions, SORT_NUMERIC ); |
105 | | - if ( count( $this->usedQuestions ) === 0 ) { |
106 | | - $this->usedQuestions = false; |
107 | | - } |
108 | | - } |
109 | | - |
110 | | - function getUsedQuestions() { |
111 | | - return $this->usedQuestions; |
112 | | - } |
113 | | - |
114 | | - function setUsedQuestions( $randomQuestions ) { |
115 | | - if ( !is_array( $randomQuestions ) ) { |
116 | | - foreach ( $this->questions as $qidx => &$question ) { |
117 | | - $question->usedId = $question->mQuestionId; |
118 | | - } |
119 | | - return; |
120 | | - } |
121 | | - sort( $randomQuestions, SORT_NUMERIC ); |
122 | | - $this->usedQuestions = array(); |
123 | | - # questions keys start from 1 |
124 | | - $usedId = 1; |
125 | | - foreach ( $this->questions as $qidx => &$question ) { |
126 | | - if ( in_array( $qidx, $randomQuestions, true ) ) { |
127 | | - # usedQuestions keys start from 0 |
128 | | - $this->usedQuestions[] = $qidx; |
129 | | - $question->usedId = $usedId++; |
130 | | - } else { |
131 | | - $question->usedId = false; |
132 | | - } |
133 | | - } |
134 | | - if ( count( $this->usedQuestions ) === 0 ) { |
135 | | - throw new MWException( 'At least one question should not be unused in ' . __METHOD__ ); |
136 | | - } |
137 | | - } |
138 | | - |
139 | | - function add( qp_AbstractQuestion $question ) { |
140 | | - if ( count( $this->questions ) === 0 ) { |
141 | | - $this->questions[1] = $question; |
142 | | - } else { |
143 | | - $this->questions[] = $question; |
144 | | - } |
145 | | - } |
146 | | - |
147 | | - function totalCount() { |
148 | | - return count( $this->questions ); |
149 | | - } |
150 | | - |
151 | | - function usedCount() { |
152 | | - $used = 0; |
153 | | - foreach ( $this->questions as &$question ) { |
154 | | - if ( $question->usedId !== false ) { |
155 | | - $used++; |
156 | | - } |
157 | | - } |
158 | | - return $used; |
159 | | - } |
160 | | - |
161 | | - /** |
162 | | - * Reset question iterator |
163 | | - */ |
164 | | - function reset() { |
165 | | - $this->qKey = 1; |
166 | | - if ( is_array( $this->usedQuestions ) ) { |
167 | | - $this->usedKey = 0; |
168 | | - } |
169 | | - } |
170 | | - |
171 | | - /** |
172 | | - * Get current question and rewind to the next question |
173 | | - * @return instance of qp_AbstractQuestion or derivative or |
174 | | - * boolean false - when there are no more questions left |
175 | | - */ |
176 | | - function iterate() { |
177 | | - if ( is_array( $this->usedQuestions ) ) { |
178 | | - while ( array_key_exists( $this->usedKey, $this->usedQuestions ) ) { |
179 | | - $qidx = $this->usedQuestions[$this->usedKey++]; |
180 | | - if ( isset( $this->questions[$qidx] ) ) { |
181 | | - return $this->questions[$qidx]; |
182 | | - } |
183 | | - } |
184 | | - return false; |
185 | | - } |
186 | | - if ( array_key_exists( $this->qKey, $this->questions ) ) { |
187 | | - $question = $this->questions[$this->qKey++]; |
188 | | - return $question; |
189 | | - } |
190 | | - return false; |
191 | | - } |
192 | | - |
193 | | -} |
Index: trunk/extensions/QPoll/qp_pollstore.php |
— | — | @@ -1,995 +0,0 @@ |
2 | | -<?php |
3 | | - |
4 | | -if ( !defined( 'MEDIAWIKI' ) ) { |
5 | | - die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
6 | | -} |
7 | | - |
8 | | -/** |
9 | | - * An interpretation result of user answer to the quiz |
10 | | - */ |
11 | | -class qp_InterpResult { |
12 | | - # short answer. it is supposed to be sortable and accountable in statistics |
13 | | - # by default, it is private (displayed only in Special:Pollresults page) |
14 | | - # blank value means short answer is unavailable |
15 | | - var $short = ''; |
16 | | - # long answer. it is supposed to be understandable by amateur users |
17 | | - # by default, it is public (displayed everywhere) |
18 | | - # blank value means long answer is unavailable |
19 | | - var $long = ''; |
20 | | - # structured answer. scalar value or an associative array. |
21 | | - # objects are not allowed. |
22 | | - # it is exported to XLS voices and can be analyzed by external tools. |
23 | | - var $structured = null; |
24 | | - # error message. non-blank value indicates interpretation script error |
25 | | - # either due to incorrect script code, or a script-generated one |
26 | | - var $error = ''; |
27 | | - # interpretation result |
28 | | - # 2d array of errors generated for [question][proposal] |
29 | | - # 3d array of errors generated for [question][proposal][category] |
30 | | - # false if no errors |
31 | | - var $qpcErrors = false; |
32 | | - |
33 | | - /** |
34 | | - * @param $init - optional array of properties to be initialized |
35 | | - */ |
36 | | - function __construct( $init = null ) { |
37 | | - $props = array( 'short', 'long', 'error' ); |
38 | | - if ( is_array( $init ) ) { |
39 | | - foreach ( $props as $prop ) { |
40 | | - if ( array_key_exists( $prop, $init ) ) { |
41 | | - $this->{ $prop } = $init[$prop]; |
42 | | - } |
43 | | - } |
44 | | - return; |
45 | | - } |
46 | | - } |
47 | | - |
48 | | - /** |
49 | | - * "global" error message |
50 | | - */ |
51 | | - function setError( $msg ) { |
52 | | - $this->error = $msg; |
53 | | - return $this; |
54 | | - } |
55 | | - |
56 | | - /** |
57 | | - * set question / proposal error message (for quizes) |
58 | | - * |
59 | | - * @param $msg string error message for [question][proposal] pair; |
60 | | - * non-string for default message |
61 | | - * @param $qidx int index of poll's question |
62 | | - * @param $pidx int index of question's proposal |
63 | | - * @param $cidx int index of proposal's category (optional) |
64 | | - */ |
65 | | - function setQPCerror( $msg, $qidx, $pidx, $cidx = null ) { |
66 | | - if ( !is_array( $this->qpcErrors ) ) { |
67 | | - $this->qpcErrors = array(); |
68 | | - } |
69 | | - if ( !array_key_exists( $qidx, $this->qpcErrors ) ) { |
70 | | - $this->qpcErrors[$qidx] = array(); |
71 | | - } |
72 | | - if ( $cidx === null ) { |
73 | | - # proposal interpretation error message |
74 | | - $this->qpcErrors[$qidx][$pidx] = $msg; |
75 | | - return; |
76 | | - } |
77 | | - # proposal's category interpretation error message |
78 | | - if ( !array_key_exists( $pidx, $this->qpcErrors[$qidx] ) || |
79 | | - !is_array( $this->qpcErrors[$qidx][$pidx] ) ) { |
80 | | - # remove previous proposal interpretation error message because |
81 | | - # now we have more precise category interpretation error message |
82 | | - $this->qpcErrors[$qidx][$pidx] = array(); |
83 | | - } |
84 | | - $this->qpcErrors[$qidx][$pidx][$cidx] = $msg; |
85 | | - } |
86 | | - |
87 | | - function setDefaultErrorMessage() { |
88 | | - if ( is_array( $this->qpcErrors ) && $this->error == '' ) { |
89 | | - $this->error = wfMsg( 'qp_interpetation_wrong_answer' ); |
90 | | - } |
91 | | - return $this; |
92 | | - } |
93 | | - |
94 | | - function isError() { |
95 | | - return $this->error != '' || is_array( $this->qpcErrors ); |
96 | | - } |
97 | | - |
98 | | -} /* end of qp_InterpResult class */ |
99 | | - |
100 | | -/** |
101 | | - * poll storage and retrieval using DB |
102 | | - * one poll may contain several questions |
103 | | - */ |
104 | | -class qp_PollStore { |
105 | | - |
106 | | - static $db = null; |
107 | | - # indicates whether random questions must be erased / regenerated when the value of |
108 | | - # 'randomize' attribute is changed from non-zero to zero and back |
109 | | - static $purgeRandomQuestions = false; |
110 | | - |
111 | | - /// DB keys |
112 | | - var $pid = null; |
113 | | - var $last_uid = null; |
114 | | - |
115 | | - # username is used for caching of setLastUser() method (which now may be called multiple times); |
116 | | - # also used by randomizer |
117 | | - var $username = ''; |
118 | | - |
119 | | - /*** common properties ***/ |
120 | | - var $mArticleId = null; |
121 | | - # unique id of poll, used for addressing, also with 'qp_' prefix as the fragment part of the link |
122 | | - var $mPollId = null; |
123 | | - # order of poll on the page |
124 | | - var $mOrderId = null; |
125 | | - |
126 | | - /*** optional attributes ***/ |
127 | | - # dependance from other poll address in the following format: "page#otherpollid" |
128 | | - var $dependsOn = null; |
129 | | - # NS & DBkey of Title object representing interpretation template for Special:Pollresults page |
130 | | - var $interpNS = 0; |
131 | | - var $interpDBkey = null; |
132 | | - # interpretation of user answer |
133 | | - var $interpResult; |
134 | | - # 1..n - number of random indexes from poll's header; 0 - poll questions are not randomized |
135 | | - # pollstore loads / saves random indexes for every user only when this property is NOT zero |
136 | | - # which improves performance of non-randomized polls |
137 | | - var $randomQuestionCount = null; |
138 | | - |
139 | | - # array of QuestionData instances (data from/to DB) |
140 | | - var $Questions = null; |
141 | | - # array of random indexes of Questions[] array (optional) |
142 | | - var $randomQuestions = false; |
143 | | - |
144 | | - # attempts of voting (passing the quiz). number of resubmits |
145 | | - # note: resubmits are counted for syntax-correct answer (when the vote is stored), |
146 | | - # yet the answer still might be logically incorrect (quiz is not passed / partially passed) |
147 | | - var $attempts = 0; |
148 | | - |
149 | | - # poll processing state, read with getState() |
150 | | - # |
151 | | - # 'NA' - object just was created |
152 | | - # |
153 | | - # 'incomplete', self::stateIncomplete() |
154 | | - # http post: not every proposals were answered: do not update DB |
155 | | - # http get: this is not the post: do not update DB |
156 | | - # |
157 | | - # 'error', self::stateError() |
158 | | - # http get: invalid question syntax, parse errors will cause submit button disabled |
159 | | - # |
160 | | - # 'complete', self::stateComplete() |
161 | | - # check whether the poll was successfully submitted |
162 | | - # store user vote to the DB (when the poll is fine) |
163 | | - # |
164 | | - var $mCompletedPostData; |
165 | | - # true, after the poll results have been successfully stored to DB |
166 | | - var $voteDone = false; |
167 | | - |
168 | | - /* $argv[ 'from' ] indicates type of construction, other elements of $argv vary according to 'from' |
169 | | - */ |
170 | | - function __construct( $argv = null ) { |
171 | | - global $wgParser; |
172 | | - if ( self::$db == null ) { |
173 | | - self::$db = & wfGetDB( DB_MASTER ); |
174 | | - } |
175 | | - $this->interpResult = new qp_InterpResult(); |
176 | | - if ( is_array( $argv ) && array_key_exists( "from", $argv ) ) { |
177 | | - $this->Questions = Array(); |
178 | | - $this->mCompletedPostData = 'NA'; |
179 | | - $this->pid = null; |
180 | | - $is_post = false; |
181 | | - switch ( $argv[ 'from' ] ) { |
182 | | - case 'poll_post' : |
183 | | - $is_post = true; |
184 | | - case 'poll_get' : |
185 | | - if ( array_key_exists( 'title', $argv ) ) { |
186 | | - $title = $argv[ 'title' ]; |
187 | | - } else { |
188 | | - $title = $wgParser->getTitle(); |
189 | | - } |
190 | | - $this->mArticleId = $title->getArticleID(); |
191 | | - $this->mPollId = $argv[ 'poll_id' ]; |
192 | | - if ( array_key_exists( 'order_id', $argv ) ) { |
193 | | - $this->mOrderId = $argv[ 'order_id' ]; |
194 | | - } |
195 | | - if ( array_key_exists( 'dependance', $argv ) && |
196 | | - $argv[ 'dependance' ] !== false ) { |
197 | | - $this->dependsOn = $argv[ 'dependance' ]; |
198 | | - } |
199 | | - if ( array_key_exists( 'interpretation', $argv ) ) { |
200 | | - # (0,'') indicates that interpretation template does not exists |
201 | | - $this->interpNS = 0; |
202 | | - $this->interpDBkey = ''; |
203 | | - if ( $argv['interpretation'] != '' ) { |
204 | | - $interp = Title::newFromText( $argv['interpretation'], NS_QP_INTERPRETATION ); |
205 | | - if ( $interp instanceof Title ) { |
206 | | - $this->interpNS = $interp->getNamespace(); |
207 | | - $this->interpDBkey = $interp->getDBkey(); |
208 | | - } |
209 | | - } |
210 | | - } |
211 | | - if ( array_key_exists( 'randomQuestionCount', $argv ) ) { |
212 | | - $this->randomQuestionCount = $argv['randomQuestionCount']; |
213 | | - } |
214 | | - # do not load / create the poll when article id is unavailable |
215 | | - # (only during newly created page submission) |
216 | | - if ( $this->mArticleId != 0 ) { |
217 | | - if ( $is_post ) { |
218 | | - $this->setPid(); |
219 | | - } else { |
220 | | - $this->loadPid(); |
221 | | - if ( is_null( $this->pid ) ) { |
222 | | - # try to create poll description (DB state was incomplete) |
223 | | - $this->setPid(); |
224 | | - } |
225 | | - } |
226 | | - } |
227 | | - break; |
228 | | - case 'pid' : |
229 | | - if ( array_key_exists( 'pid', $argv ) ) { |
230 | | - $pid = intval( $argv[ 'pid' ] ); |
231 | | - $res = self::$db->select( 'qp_poll_desc', |
232 | | - array( 'article_id', 'poll_id', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
233 | | - array( 'pid' => $pid ), |
234 | | - __METHOD__ . ":create from pid" ); |
235 | | - $row = self::$db->fetchObject( $res ); |
236 | | - if ( $row === false ) { |
237 | | - throw new MWException( 'Attempt to create poll from non-existent poll id in ' . __METHOD__ ); |
238 | | - } |
239 | | - $this->pid = $pid; |
240 | | - $this->mArticleId = $row->article_id; |
241 | | - $this->mPollId = $row->poll_id; |
242 | | - $this->mOrderId = $row->order_id; |
243 | | - $this->dependsOn = $row->dependance; |
244 | | - $this->interpNS = $row->interpretation_namespace; |
245 | | - $this->interpDBkey = $row->interpretation_title; |
246 | | - $this->randomQuestionCount = $row->random_question_count; |
247 | | - } |
248 | | - break; |
249 | | - } |
250 | | - } |
251 | | - } |
252 | | - |
253 | | - // special version of constructor that builds pollstore from the given poll address |
254 | | - // @return instance of qp_PollStore on success, false on error |
255 | | - static function newFromAddr( $pollAddr ) { |
256 | | - # build poll object from given poll address in args[0] |
257 | | - $pollAddr = qp_AbstractPoll::getPrefixedPollAddress( $pollAddr ); |
258 | | - if ( is_array( $pollAddr ) ) { |
259 | | - list( $pollTitleStr, $pollId ) = $pollAddr; |
260 | | - $pollTitle = Title::newFromURL( $pollTitleStr ); |
261 | | - if ( $pollTitle !== null ) { |
262 | | - $pollArticleId = intval( $pollTitle->getArticleID() ); |
263 | | - if ( $pollArticleId > 0 ) { |
264 | | - return new qp_PollStore( array( |
265 | | - 'from' => 'poll_get', |
266 | | - 'title' => $pollTitle, |
267 | | - 'poll_id' => $pollId ) ); |
268 | | - } else { |
269 | | - return qp_Setup::ERROR_MISSED_TITLE; |
270 | | - } |
271 | | - } else { |
272 | | - return qp_Setup::ERROR_MISSED_TITLE; |
273 | | - } |
274 | | - } else { |
275 | | - return qp_Setup::ERROR_INVALID_ADDRESS; |
276 | | - } |
277 | | - } |
278 | | - |
279 | | - /** |
280 | | - * qdata instantiator |
281 | | - * Please use it instead of qdata constructors |
282 | | - */ |
283 | | - static function newQuestionData( $argv ) { |
284 | | - switch ( $argv['type'] ) { |
285 | | - case 'textQuestion' : |
286 | | - return new qp_TextQuestionData( $argv ); |
287 | | - case 'singleChoice' : |
288 | | - case 'multipleChoice' : |
289 | | - case 'mixedChoice' : |
290 | | - return new qp_QuestionData( $argv ); |
291 | | - default : |
292 | | - throw new MWException( 'Unknown type of question ' . qp_Setup::specialchars( $argv['type'] ) . ' in ' . __METHOD__ ); |
293 | | - } |
294 | | - } |
295 | | - |
296 | | - function getPollId() { |
297 | | - return $this->mPollId; |
298 | | - } |
299 | | - |
300 | | - # returns Title object, to get a URI path, use Title::getFullText()/getPrefixedText() on it |
301 | | - function getTitle() { |
302 | | - if ( $this->mArticleId === 0 ) { |
303 | | - throw new MWException( __METHOD__ . ' cannot be called for unsaved new pages' ); |
304 | | - } |
305 | | - if ( is_null( $this->mArticleId ) ) { |
306 | | - throw new MWException( 'Unknown article id in ' . __METHOD__ ); |
307 | | - } |
308 | | - if ( is_null( $this->mPollId ) ) { |
309 | | - throw new MWException( 'Unknown poll id in ' . __METHOD__ ); |
310 | | - } |
311 | | - $res = Title::newFromID( $this->mArticleId ); |
312 | | - $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) ); |
313 | | - if ( !( $res instanceof Title ) ) { |
314 | | - throw new MWException( 'Invalid title created in ' . __METHOD__ ); |
315 | | - } |
316 | | - return $res; |
317 | | - } |
318 | | - |
319 | | - /** |
320 | | - * @return Title instance of interpretation template |
321 | | - */ |
322 | | - function getInterpTitle() { |
323 | | - $title = Title::newFromText( $this->interpDBkey, $this->interpNS ); |
324 | | - return ( $title instanceof Title ) ? $title : null; |
325 | | - } |
326 | | - |
327 | | - // warning: will work only after successful loadUserAlreadyVoted() or loadUserVote() |
328 | | - function isAlreadyVoted() { |
329 | | - if ( is_array( $this->Questions ) && count( $this->Questions > 0 ) ) { |
330 | | - foreach ( $this->Questions as &$qdata ) { |
331 | | - if ( $qdata->alreadyVoted ) |
332 | | - return true; |
333 | | - } |
334 | | - } |
335 | | - return false; |
336 | | - } |
337 | | - |
338 | | - # checks whether the question with specified id exists in the poll store |
339 | | - # @return boolean, true when the question exists |
340 | | - function questionExists( $question_id ) { |
341 | | - return array_key_exists( $question_id, $this->Questions ); |
342 | | - } |
343 | | - |
344 | | - # load questions for the newly created poll (if the poll was voted at least once) |
345 | | - # @return boolean, true when the questions are available, false otherwise (poll was never voted) |
346 | | - function loadQuestions() { |
347 | | - $result = false; |
348 | | - $typeFromVer0_5 = array( |
349 | | - "singleChoicePoll" => "singleChoice", |
350 | | - "multipleChoicePoll" => "multipleChoice", |
351 | | - "mixedChoicePoll" => "mixedChoice" |
352 | | - ); |
353 | | - if ( $this->pid !== null ) { |
354 | | - $res = self::$db->select( 'qp_question_desc', |
355 | | - array( 'question_id', 'type', 'common_question' ), |
356 | | - array( 'pid' => $this->pid ), |
357 | | - __METHOD__ ); |
358 | | - if ( self::$db->numRows( $res ) > 0 ) { |
359 | | - $result = true; |
360 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
361 | | - $question_id = intval( $row->question_id ); |
362 | | - # convert old (v0.5) question type string to the "new" type string |
363 | | - if ( isset( $typeFromVer0_5[$row->type] ) ) { |
364 | | - $row->type = $typeFromVer0_5[$row->type]; |
365 | | - } |
366 | | - # create a qp_QuestionData object from DB fields |
367 | | - $this->Questions[ $question_id ] = self::newQuestionData( array( |
368 | | - 'from' => 'qid', |
369 | | - 'qid' => $question_id, |
370 | | - 'type' => $row->type, |
371 | | - 'common_question' => $row->common_question ) ); |
372 | | - } |
373 | | - $this->getCategories(); |
374 | | - $this->getProposalText(); |
375 | | - } |
376 | | - } |
377 | | - return $result; |
378 | | - } |
379 | | - |
380 | | - /** |
381 | | - * iterates through the list of users who voted the current poll |
382 | | - * @return mixed false on failure, array of (uid=>username) on success (might be empty) |
383 | | - */ |
384 | | - function pollVotersPager( $offset = 0, $limit = 20 ) { |
385 | | - if ( $this->pid === null ) { |
386 | | - return false; |
387 | | - } |
388 | | - $qp_users_polls = self::$db->tableName( 'qp_users_polls' ); |
389 | | - $qp_users = self::$db->tableName( 'qp_users' ); |
390 | | - $query = "SELECT qup.uid AS uid, name AS username " . |
391 | | - "FROM $qp_users_polls qup " . |
392 | | - "INNER JOIN $qp_users qu ON qup.uid = qu.uid " . |
393 | | - "WHERE pid = " . intval( $this->pid ) . " " . |
394 | | - "LIMIT " . intval( $offset ) . ", " . intval( $limit ); |
395 | | - $res = self::$db->query( $query, __METHOD__ ); |
396 | | - $result = array(); |
397 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
398 | | - $result[intval( $row->uid )] = $row->username; |
399 | | - } |
400 | | - return $result; |
401 | | - } |
402 | | - |
403 | | - /** |
404 | | - * returns voices of the selected users in the selected question of current poll |
405 | | - * @param $uids array of user id's in DB |
406 | | - * @return mixed array [uid][proposal_id][cat_id]=text_answer on success, |
407 | | - * false on failure |
408 | | - */ |
409 | | - function questionVoicesRange( $question_id, array $uids ) { |
410 | | - if ( $this->pid === null ) { |
411 | | - return false; |
412 | | - } |
413 | | - $qp_question_answers = self::$db->tableName( 'qp_question_answers' ); |
414 | | - $query = "SELECT uid, proposal_id, cat_id, text_answer " . |
415 | | - "FROM $qp_question_answers " . |
416 | | - "WHERE pid = " . intval( $this->pid ) . " AND question_id = " . intval( $question_id ) . " AND uid IN (" . implode( ',', array_map( 'intval', $uids ) ) . ") " . |
417 | | - "ORDER BY uid"; |
418 | | - $res = self::$db->query( $query, __METHOD__ ); |
419 | | - $result = array(); |
420 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
421 | | - $uid = intval( $row->uid ); |
422 | | - if ( !isset( $result[$uid] ) ) { |
423 | | - $result[$uid] = array(); |
424 | | - } |
425 | | - $proposal_id = intval( $row->proposal_id ); |
426 | | - if ( !isset( $result[$uid][$proposal_id] ) ) { |
427 | | - $result[$uid][$proposal_id] = array(); |
428 | | - } |
429 | | - $result[$uid][$proposal_id][intval( $row->cat_id )] = ( ( $row->text_answer == "" ) ? "+" : $row->text_answer ); |
430 | | - } |
431 | | - return $result; |
432 | | - } |
433 | | - |
434 | | - // checks whether single user already voted the poll's questions |
435 | | - // will be written into self::Questions[]->alreadyVoted |
436 | | - // may be used only after loadQuestions() |
437 | | - // returns true when the user voted to any of the currently defined questions, false otherwise |
438 | | - function loadUserAlreadyVoted() { |
439 | | - $result = false; |
440 | | - if ( $this->pid === null || $this->last_uid === null || |
441 | | - !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) { |
442 | | - return false; |
443 | | - } |
444 | | - $res = self::$db->select( 'qp_question_answers', |
445 | | - array( 'DISTINCT question_id' ), |
446 | | - array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
447 | | - __METHOD__ . ':load one user poll questions alreadyVoted values' ); |
448 | | - if ( self::$db->numRows( $res ) == 0 ) { |
449 | | - return false; |
450 | | - } |
451 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
452 | | - $question_id = intval( $row->question_id ); |
453 | | - if ( $this->questionExists( $question_id ) ) { |
454 | | - $result = $this->Questions[ $question_id ]->alreadyVoted = true; |
455 | | - } |
456 | | - } |
457 | | - return $result; |
458 | | - } |
459 | | - |
460 | | - // load single user vote |
461 | | - // also loads short & long answer interpretation, when available |
462 | | - // will be written into self::Questions[]->ProposalCategoryId,ProposalCategoryText,alreadyVoted |
463 | | - // may be used only after loadQuestions() |
464 | | - // returns true when any of currently defined questions has the votes, false otherwise |
465 | | - function loadUserVote() { |
466 | | - $result = false; |
467 | | - if ( $this->pid === null || $this->last_uid === null || |
468 | | - !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) { |
469 | | - return false; |
470 | | - } |
471 | | - $res = self::$db->select( 'qp_question_answers', |
472 | | - array( 'question_id', 'proposal_id', 'cat_id', 'text_answer' ), |
473 | | - array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
474 | | - __METHOD__ . ':load one user single poll vote' ); |
475 | | - if ( self::$db->numRows( $res ) == 0 ) { |
476 | | - return false; |
477 | | - } |
478 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
479 | | - $question_id = intval( $row->question_id ); |
480 | | - if ( $this->questionExists( $question_id ) ) { |
481 | | - $qdata = &$this->Questions[ $question_id ]; |
482 | | - $result = $qdata->alreadyVoted = true; |
483 | | - $qdata->ProposalCategoryId[ intval( $row->proposal_id ) ][] = intval( $row->cat_id ); |
484 | | - $qdata->ProposalCategoryText[ intval( $row->proposal_id ) ][] = $row->text_answer; |
485 | | - } |
486 | | - } |
487 | | - return $result; |
488 | | - } |
489 | | - |
490 | | - // load voting statistics (totals) from DB |
491 | | - // input: $questions_set is optional array of integer question_id values of the current poll |
492 | | - // output: $this->Questions[]Votes[] is set on success |
493 | | - function loadTotals( $questions_set = false ) { |
494 | | - if ( $this->pid !== null && |
495 | | - is_array( $this->Questions ) && count( $this->Questions > 0 ) ) { |
496 | | - $where = 'pid=' . self::$db->addQuotes( $this->pid ); |
497 | | - if ( is_array( $questions_set ) ) { |
498 | | - $where .= ' AND question_id IN ('; |
499 | | - $first_elem = true; |
500 | | - foreach ( $questions_set as &$qid ) { |
501 | | - if ( $first_elem ) { |
502 | | - $first_elem = false; |
503 | | - } else { |
504 | | - $where .= ','; |
505 | | - } |
506 | | - $where .= self::$db->addQuotes( $qid ); |
507 | | - } |
508 | | - $where .= ')'; |
509 | | - } |
510 | | - $res = self::$db->select( 'qp_question_answers', |
511 | | - array( 'count(uid)', 'question_id', 'proposal_id', 'cat_id' ), |
512 | | - $where, |
513 | | - __METHOD__ . ':load single poll count of user votes', |
514 | | - array( 'GROUP BY' => 'question_id,proposal_id,cat_id' ) ); |
515 | | - while ( $row = self::$db->fetchRow( $res ) ) { |
516 | | - $question_id = intval( $row[ "question_id" ] ); |
517 | | - $propkey = intval( $row[ "proposal_id" ] ); |
518 | | - $catkey = intval( $row[ "cat_id" ] ); |
519 | | - if ( $this->questionExists( $question_id ) ) { |
520 | | - $qdata = &$this->Questions[ $question_id ]; |
521 | | - if ( !is_array( $qdata->Votes ) ) { |
522 | | - $qdata->Votes = Array(); |
523 | | - } |
524 | | - if ( !array_key_exists( $propkey, $qdata->Votes ) ) { |
525 | | - $qdata->Votes[ $propkey ] = array_fill( 0, count( $qdata->Categories ), 0 ); |
526 | | - } |
527 | | - $qdata->Votes[ $propkey ][ $catkey ] = intval( $row[ "count(uid)" ] ); |
528 | | - } |
529 | | - } |
530 | | - } |
531 | | - } |
532 | | - |
533 | | - function totalUsersAnsweredQuestion( &$qdata ) { |
534 | | - $result = 0; |
535 | | - if ( $this->pid !== null ) { |
536 | | - $res = self::$db->select( 'qp_question_answers', |
537 | | - array( 'count(distinct uid)' ), |
538 | | - array( 'pid' => $this->pid, 'question_id' => $qdata->question_id ), |
539 | | - __METHOD__ ); |
540 | | - if ( $row = self::$db->fetchRow( $res ) ) { |
541 | | - $result = intval( $row[ "count(distinct uid)" ] ); |
542 | | - } |
543 | | - } |
544 | | - return $result; |
545 | | - } |
546 | | - |
547 | | - // try to calculate percents for every question where Votes[] are available |
548 | | - function calculateStatistics() { |
549 | | - foreach ( $this->Questions as &$qdata ) { |
550 | | - $this->calculateQuestionStatistics( $qdata ); |
551 | | - } |
552 | | - } |
553 | | - |
554 | | - // try to calculate percents for the one question |
555 | | - private function calculateQuestionStatistics( &$qdata ) { |
556 | | - if ( isset( $qdata->Votes ) ) { // is "votable" |
557 | | - $qdata->restoreSpans(); |
558 | | - $spansUsed = count( $qdata->CategorySpans ) > 0 ; |
559 | | - foreach ( $qdata->ProposalText as $propkey => $proposal_text ) { |
560 | | - if ( isset( $qdata->Votes[ $propkey ] ) ) { |
561 | | - $votes_row = &$qdata->Votes[ $propkey ]; |
562 | | - if ( $qdata->type == "singleChoice" ) { |
563 | | - if ( $spansUsed ) { |
564 | | - $row_totals = array_fill( 0, count( $qdata->CategorySpans ), 0 ); |
565 | | - } else { |
566 | | - $votes_total = 0; |
567 | | - } |
568 | | - foreach ( $qdata->Categories as $catkey => $cat ) { |
569 | | - if ( isset( $votes_row[ $catkey ] ) ) { |
570 | | - if ( $spansUsed ) { |
571 | | - $row_totals[ intval( $cat[ "spanId" ] ) ] += $votes_row[ $catkey ]; |
572 | | - } else { |
573 | | - $votes_total += $votes_row[ $catkey ]; |
574 | | - } |
575 | | - } |
576 | | - } |
577 | | - } else { |
578 | | - $votes_total = $this->totalUsersAnsweredQuestion( $qdata ); |
579 | | - } |
580 | | - foreach ( $qdata->Categories as $catkey => $cat ) { |
581 | | - $num_of_votes = ''; |
582 | | - if ( isset( $votes_row[ $catkey ] ) ) { |
583 | | - $num_of_votes = $votes_row[ $catkey ]; |
584 | | - if ( $spansUsed ) { |
585 | | - if ( isset( $qdata->Categories[ $catkey ][ "spanId" ] ) ) { |
586 | | - $votes_total = $row_totals[ intval( $qdata->Categories[ $catkey ][ "spanId" ] ) ]; |
587 | | - } |
588 | | - } |
589 | | - } |
590 | | - $qdata->Percents[ $propkey ][ $catkey ] = ( $votes_total > 0 ) ? (float) $num_of_votes / (float) $votes_total : 0.0; |
591 | | - } |
592 | | - } |
593 | | - } |
594 | | - } |
595 | | - } |
596 | | - |
597 | | - private function getCategories() { |
598 | | - $res = self::$db->select( 'qp_question_categories', |
599 | | - array( 'question_id', 'cat_id', 'cat_name' ), |
600 | | - array( 'pid' => $this->pid ), |
601 | | - __METHOD__ ); |
602 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
603 | | - $question_id = intval( $row->question_id ); |
604 | | - $cat_id = intval( $row->cat_id ); |
605 | | - if ( $this->questionExists( $question_id ) ) { |
606 | | - $qdata = &$this->Questions[ $question_id ]; |
607 | | - $qdata->Categories[ $cat_id ][ "name" ] = $row->cat_name; |
608 | | - } |
609 | | - } |
610 | | - foreach ( $this->Questions as &$qdata ) { |
611 | | - $qdata->restoreSpans(); |
612 | | - } |
613 | | - } |
614 | | - |
615 | | - private function getProposalText() { |
616 | | - $res = self::$db->select( 'qp_question_proposals', |
617 | | - array( 'question_id', 'proposal_id', 'proposal_text' ), |
618 | | - array( 'pid' => $this->pid ), |
619 | | - __METHOD__ ); |
620 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
621 | | - $question_id = intval( $row->question_id ); |
622 | | - $proposal_id = intval( $row->proposal_id ); |
623 | | - if ( $this->questionExists( $question_id ) ) { |
624 | | - $qdata = &$this->Questions[ $question_id ]; |
625 | | - $qdata->ProposalText[ $proposal_id ] = $row->proposal_text; |
626 | | - } |
627 | | - } |
628 | | - } |
629 | | - |
630 | | - function getState() { |
631 | | - return $this->mCompletedPostData; |
632 | | - } |
633 | | - |
634 | | - function stateIncomplete() { |
635 | | - if ( $this->mCompletedPostData == 'NA' ) { |
636 | | - $this->mCompletedPostData = 'incomplete'; |
637 | | - } |
638 | | - } |
639 | | - |
640 | | - function stateError() { |
641 | | - $this->mCompletedPostData = 'error'; |
642 | | - } |
643 | | - |
644 | | - # check whether the poll was successfully submitted |
645 | | - # @return boolean - result of operation |
646 | | - function stateComplete() { |
647 | | - # completed only when previous state was unavaibale; error state can't be completed |
648 | | - if ( $this->mCompletedPostData == 'NA' && count( $this->Questions ) > 0 ) { |
649 | | - $this->mCompletedPostData = 'complete'; |
650 | | - return true; |
651 | | - } else { |
652 | | - return false; |
653 | | - } |
654 | | - } |
655 | | - |
656 | | - /** |
657 | | - * Checks, whether particular question belongs to user's random seed |
658 | | - * @param $question_id question_id from DB |
659 | | - * @return true: question belongs to the seed; |
660 | | - * false: question does not belong to the seed; |
661 | | - */ |
662 | | - function isUsedQuestion( $question_id ) { |
663 | | - return !is_array( $this->randomQuestions ) || |
664 | | - in_array( $question_id, $this->randomQuestions, true ); |
665 | | - } |
666 | | - |
667 | | - /** |
668 | | - * Loads $this->randomQuestions from DB for current user |
669 | | - * Will be overriden in memory when number of random questions was changed |
670 | | - */ |
671 | | - function loadRandomQuestions() { |
672 | | - if ( $this->mArticleId == 0 ) { |
673 | | - $this->randomQuestions = false; |
674 | | - return; |
675 | | - } |
676 | | - if ( is_null( $this->pid ) ) { |
677 | | - throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
678 | | - } |
679 | | - if ( is_null( $this->last_uid ) ) { |
680 | | - throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
681 | | - } |
682 | | - $res = self::$db->select( 'qp_random_questions', 'question_id', array( 'uid' => $this->last_uid, 'pid' => $this->pid ), __METHOD__ ); |
683 | | - $this->randomQuestions = array(); |
684 | | - while ( $row = self::$db->fetchObject( $res ) ) { |
685 | | - $this->randomQuestions[] = intval( $row->question_id ); |
686 | | - } |
687 | | - if ( count( $this->randomQuestions ) === 0 ) { |
688 | | - $this->randomQuestions = false; |
689 | | - } else { |
690 | | - sort( $this->randomQuestions, SORT_NUMERIC ); |
691 | | - } |
692 | | - } |
693 | | - |
694 | | - /** |
695 | | - * Stores $this->randomQuestions into DB |
696 | | - * Should be called: |
697 | | - * when user views the page with the poll first time |
698 | | - * when number of random questions for poll was changed |
699 | | - */ |
700 | | - function setRandomQuestions() { |
701 | | - if ( $this->mArticleId == 0 ) { |
702 | | - return; |
703 | | - } |
704 | | - if ( is_null( $this->pid ) ) { |
705 | | - throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
706 | | - } |
707 | | - if ( is_null( $this->last_uid ) ) { |
708 | | - throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
709 | | - } |
710 | | - if ( is_array( $this->randomQuestions ) ) { |
711 | | - $data = array(); |
712 | | - foreach ( $this->randomQuestions as $qidx ) { |
713 | | - $data[] = array( 'pid' => $this->pid, 'uid' => $this->last_uid, 'question_id' => $qidx ); |
714 | | - } |
715 | | - self::$db->begin(); |
716 | | - self::$db->delete( 'qp_random_questions', |
717 | | - array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
718 | | - __METHOD__ ); |
719 | | - $res = self::$db->insert( 'qp_random_questions', |
720 | | - $data, |
721 | | - __METHOD__ . ':set random questions seed' ); |
722 | | - self::$db->commit(); |
723 | | - return; |
724 | | - } |
725 | | - # this->randomQuestions === false; this poll is not randomized anymore |
726 | | - self::$db->delete( 'qp_random_questions', |
727 | | - array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
728 | | - __METHOD__ . ':remove question random seed' |
729 | | - ); |
730 | | - } |
731 | | - |
732 | | - function setLastUser( $username, $store_new_user_to_db = true ) { |
733 | | - if ( $this->pid === null ) { |
734 | | - return; |
735 | | - } |
736 | | - # do no query DB for the same user more than once |
737 | | - if ( $this->username === $username ) { |
738 | | - return; |
739 | | - } |
740 | | - $res = self::$db->select( 'qp_users', 'uid', array( 'name' => $username ), __METHOD__ ); |
741 | | - $row = self::$db->fetchObject( $res ); |
742 | | - if ( $row === false ) { |
743 | | - if ( $store_new_user_to_db ) { |
744 | | - self::$db->insert( 'qp_users', array( 'name' => $username ), __METHOD__ . ':UpdateUser' ); |
745 | | - $this->last_uid = intval( self::$db->insertId() ); |
746 | | - # set username, user was created |
747 | | - $this->username = $username; |
748 | | - } else { |
749 | | - $this->last_uid = null; |
750 | | - return; |
751 | | - } |
752 | | - } else { |
753 | | - $this->last_uid = intval( $row->uid ); |
754 | | - # set username, used was loaded |
755 | | - $this->username = $username; |
756 | | - } |
757 | | - $res = self::$db->select( 'qp_users_polls', |
758 | | - array( 'attempts', 'short_interpretation', 'long_interpretation' ), |
759 | | - array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
760 | | - __METHOD__ . ':load short & long answer interpretation' ); |
761 | | - if ( self::$db->numRows( $res ) != 0 ) { |
762 | | - $row = self::$db->fetchObject( $res ); |
763 | | - $this->attempts = $row->attempts; |
764 | | - $this->interpResult = new qp_InterpResult(); |
765 | | - $this->interpResult->short = $row->short_interpretation; |
766 | | - $this->interpResult->long = $row->long_interpretation; |
767 | | - } |
768 | | - $this->randomQuestions = false; |
769 | | - if ( $this->randomQuestionCount != 0 ) { |
770 | | - $this->loadRandomQuestions(); |
771 | | - } |
772 | | -// todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed |
773 | | - } |
774 | | - |
775 | | - function getUserName( $uid ) { |
776 | | - if ( $uid !== null ) { |
777 | | - $res = self::$db->select( 'qp_users', 'name', 'uid=' . self::$db->addQuotes( intval( $uid ) ), __METHOD__ ); |
778 | | - $row = self::$db->fetchObject( $res ); |
779 | | - if ( $row != false ) { |
780 | | - return $row->name; |
781 | | - } |
782 | | - } |
783 | | - return false; |
784 | | - } |
785 | | - |
786 | | - private function loadPid() { |
787 | | - if ( $this->mArticleId === 0 ) { |
788 | | - return; |
789 | | - } |
790 | | - $res = self::$db->select( 'qp_poll_desc', |
791 | | - array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
792 | | - array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId ), |
793 | | - __METHOD__ ); |
794 | | - $row = self::$db->fetchObject( $res ); |
795 | | - if ( $row != false ) { |
796 | | - $this->pid = $row->pid; |
797 | | - # some constructors don't supply the poll attributes, get the values from DB in such case |
798 | | - if ( $this->mOrderId === null ) { |
799 | | - $this->mOrderId = $row->order_id; |
800 | | - } |
801 | | - if ( $this->dependsOn === null ) { |
802 | | - $this->dependsOn = $row->dependance; |
803 | | - } |
804 | | - if ( $this->interpDBkey === null ) { |
805 | | - $this->interpNS = $row->interpretation_namespace; |
806 | | - $this->interpDBkey = $row->interpretation_title; |
807 | | - } |
808 | | - if ( is_null( $this->randomQuestionCount ) ) { |
809 | | - $this->randomQuestionCount = $row->random_question_count; |
810 | | - } |
811 | | - $this->updatePollAttributes( $row ); |
812 | | - } |
813 | | - } |
814 | | - |
815 | | - private function setPid() { |
816 | | - if ( $this->mArticleId === 0 ) { |
817 | | - throw new MWException( 'Cannot save new poll description during new page preprocess in ' . __METHOD__ ); |
818 | | - } |
819 | | - $res = self::$db->select( 'qp_poll_desc', |
820 | | - array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
821 | | - 'article_id=' . self::$db->addQuotes( $this->mArticleId ) . ' and ' . |
822 | | - 'poll_id=' . self::$db->addQuotes( $this->mPollId ) ); |
823 | | - $row = self::$db->fetchObject( $res ); |
824 | | - if ( $row == false ) { |
825 | | - self::$db->insert( 'qp_poll_desc', |
826 | | - array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ), |
827 | | - __METHOD__ . ':update poll' ); |
828 | | - $this->pid = self::$db->insertId(); |
829 | | - } else { |
830 | | - $this->pid = $row->pid; |
831 | | - $this->updatePollAttributes( $row ); |
832 | | - } |
833 | | -// todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed |
834 | | - } |
835 | | - |
836 | | - private function updatePollAttributes( $row ) { |
837 | | - self::$db->begin(); |
838 | | - if ( $this->mOrderId != $row->order_id || |
839 | | - $this->dependsOn != $row->dependance || |
840 | | - $this->interpNS != $row->interpretation_namespace || |
841 | | - $this->interpDBkey != $row->interpretation_title || |
842 | | - $this->randomQuestionCount != $row->random_question_count ) { |
843 | | - $res = self::$db->replace( 'qp_poll_desc', |
844 | | - array( 'poll', 'article_poll' ), |
845 | | - array( 'pid' => $this->pid, 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ), |
846 | | - __METHOD__ . ':poll attributes update' |
847 | | - ); |
848 | | - } |
849 | | - if ( $this->randomQuestionCount != $row->random_question_count && |
850 | | - $this->randomQuestionCount == 0 && |
851 | | - self::$purgeRandomQuestions ) { |
852 | | - # the poll questions are not randomized anymore |
853 | | - self::$db->delete( 'qp_random_questions', |
854 | | - array( 'pid' => $this->pid ), |
855 | | - __METHOD__ . ':delete unused random seeds' ); |
856 | | - } |
857 | | - self::$db->commit(); |
858 | | - } |
859 | | - |
860 | | - private function setQuestionDesc() { |
861 | | - $insert = array(); |
862 | | - foreach ( $this->Questions as $qkey => &$ques ) { |
863 | | - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $ques->CommonQuestion ); |
864 | | - $ques->question_id = $qkey; |
865 | | - } |
866 | | - if ( count( $insert ) > 0 ) { |
867 | | - self::$db->replace( 'qp_question_desc', |
868 | | - array( 'question' ), |
869 | | - $insert, |
870 | | - __METHOD__ ); |
871 | | - } |
872 | | - } |
873 | | - |
874 | | - private function setCategories() { |
875 | | - $insert = Array(); |
876 | | - foreach ( $this->Questions as $qkey => &$ques ) { |
877 | | - $ques->packSpans(); |
878 | | - foreach ( $ques->Categories as $catkey => &$Cat ) { |
879 | | - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'cat_id' => $catkey, 'cat_name' => $Cat["name"] ); |
880 | | - } |
881 | | - $ques->restoreSpans(); |
882 | | - } |
883 | | - if ( count( $insert ) > 0 ) { |
884 | | - self::$db->replace( 'qp_question_categories', |
885 | | - array( 'category' ), |
886 | | - $insert, |
887 | | - __METHOD__ ); |
888 | | - } |
889 | | - } |
890 | | - |
891 | | - private function setProposals() { |
892 | | - global $wgContLang; |
893 | | - $insert = Array(); |
894 | | - foreach ( $this->Questions as $qkey => &$ques ) { |
895 | | - foreach ( $ques->ProposalText as $propkey => $ptext ) { |
896 | | - $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$proposal_max_length , '' ) ); |
897 | | - } |
898 | | - } |
899 | | - if ( count( $insert ) > 0 ) { |
900 | | - self::$db->replace( 'qp_question_proposals', |
901 | | - array( 'proposal' ), |
902 | | - $insert, |
903 | | - __METHOD__ ); |
904 | | - } |
905 | | - } |
906 | | - |
907 | | - /** |
908 | | - * Prepares an array of user answer to the current poll and interprets these |
909 | | - * Stores the result in $this->interpResult |
910 | | - */ |
911 | | - private function interpretVote() { |
912 | | - $this->interpResult = new qp_InterpResult(); |
913 | | - $interpTitle = $this->getInterpTitle(); |
914 | | - if ( $interpTitle === null ) { |
915 | | - return; |
916 | | - } |
917 | | - $interpArticle = new Article( $interpTitle, 0 ); |
918 | | - if ( !$interpArticle->exists() ) { |
919 | | - return; |
920 | | - } |
921 | | - |
922 | | - # prepare array of user answers that will be passed to the interpreter |
923 | | - $poll_answer = array(); |
924 | | - |
925 | | - foreach ( $this->Questions as &$qdata ) { |
926 | | - $questions = array(); |
927 | | - foreach ( $qdata->ProposalText as $propkey => &$proposal_text ) { |
928 | | - $proposals = array(); |
929 | | - foreach ( $qdata->Categories as $catkey => &$cat_name ) { |
930 | | - $text_answer = ''; |
931 | | - if ( array_key_exists( $propkey, $qdata->ProposalCategoryId ) && |
932 | | - ( $id_key = array_search( $catkey, $qdata->ProposalCategoryId[ $propkey ] ) ) !== false ) { |
933 | | - $proposals[$catkey] = $qdata->ProposalCategoryText[ $propkey ][ $id_key ]; |
934 | | - } |
935 | | - } |
936 | | - $questions[$propkey] = $proposals; |
937 | | - } |
938 | | - if ( $this->isUsedQuestion( $qdata->question_id ) ) { |
939 | | - $poll_answer[$qdata->question_id] = $questions; |
940 | | - } |
941 | | - } |
942 | | - |
943 | | - # interpret the poll answer to get interpretation answer |
944 | | - $this->interpResult = qp_Interpret::getResult( $interpArticle, array( 'answer' => $poll_answer, 'randomQuestions' => $this->randomQuestions ) ); |
945 | | - } |
946 | | - |
947 | | - // warning: requires qp_PollStorage::last_uid to be set |
948 | | - private function setAnswers() { |
949 | | - $insert = Array(); |
950 | | - foreach ( $this->Questions as $qkey => &$ques ) { |
951 | | - foreach ( $ques->ProposalCategoryId as $propkey => &$prop_answers ) { |
952 | | - foreach ( $prop_answers as $idkey => $catkey ) { |
953 | | - $insert[] = array( 'uid' => $this->last_uid, 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'cat_id' => $catkey, 'text_answer' => $ques->ProposalCategoryText[ $propkey ][ $idkey ] ); |
954 | | - } |
955 | | - } |
956 | | - } |
957 | | - # TODO: delete votes of all users, when the POST question header is incompatible with question header in DB ? |
958 | | - # delete previous vote to make sure previous header of this poll was not incompatible with current vote |
959 | | - self::$db->delete( 'qp_question_answers', |
960 | | - array( 'uid' => $this->last_uid, 'pid' => $this->pid ), |
961 | | - __METHOD__ . ':delete previous answers of current user to the same poll' |
962 | | - ); |
963 | | - # vote |
964 | | - if ( count( $insert ) > 0 ) { |
965 | | - self::$db->replace( 'qp_question_answers', |
966 | | - array( 'answer' ), |
967 | | - $insert, |
968 | | - __METHOD__ ); |
969 | | - $this->interpretVote(); |
970 | | - # update interpretation result and number of syntax-valid resubmit attempts |
971 | | - $qp_users_polls = self::$db->tableName( 'qp_users_polls' ); |
972 | | - $short = self::$db->addQuotes( $this->interpResult->short ); |
973 | | - $long = self::$db->addQuotes( $this->interpResult->long ); |
974 | | - $this->attempts++; |
975 | | - $stmt = "INSERT INTO {$qp_users_polls} (uid,pid,short_interpretation,long_interpretation)\n VALUES ( " . intval( $this->last_uid ) . ", " . intval( $this->pid ) . ", {$short}, {$long} )\n ON DUPLICATE KEY UPDATE attempts = " . intval( $this->attempts ) . ", short_interpretation = {$short} , long_interpretation = {$long}"; |
976 | | - self::$db->query( $stmt, __METHOD__ ); |
977 | | - } |
978 | | - } |
979 | | - |
980 | | - # when the user votes and poll wasn't previousely voted yet, it also creates the poll structures in DB |
981 | | - function setUserVote() { |
982 | | - if ( $this->pid !== null && |
983 | | - $this->last_uid !== null && |
984 | | - $this->mCompletedPostData == "complete" && |
985 | | - is_array( $this->Questions ) && count( $this->Questions ) > 0 ) { |
986 | | - self::$db->begin(); |
987 | | - $this->setQuestionDesc(); |
988 | | - $this->setCategories(); |
989 | | - $this->setProposals(); |
990 | | - $this->setAnswers(); |
991 | | - self::$db->commit(); |
992 | | - $this->voteDone = true; |
993 | | - } |
994 | | - } |
995 | | - |
996 | | -} |
Index: trunk/extensions/QPoll/qp_eval.php |
— | — | @@ -1,412 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * ***** BEGIN LICENSE BLOCK ***** |
5 | | - * This file is part of QPoll. |
6 | | - * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
7 | | - * |
8 | | - * QPoll is free software; you can redistribute it and/or modify |
9 | | - * it under the terms of the GNU General Public License as published by |
10 | | - * the Free Software Foundation; either version 2 of the License, or |
11 | | - * (at your option) any later version. |
12 | | - * |
13 | | - * QPoll is distributed in the hope that it will be useful, |
14 | | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | | - * GNU General Public License for more details. |
17 | | - * |
18 | | - * You should have received a copy of the GNU General Public License |
19 | | - * along with QPoll; if not, write to the Free Software |
20 | | - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
21 | | - * |
22 | | - * ***** END LICENSE BLOCK ***** |
23 | | - * |
24 | | - * QPoll is a poll tool for MediaWiki. |
25 | | - * |
26 | | - * To activate this extension : |
27 | | - * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
28 | | - * * Place the files from the extension archive there. |
29 | | - * * Add this line at the end of your LocalSettings.php file : |
30 | | - * require_once "$IP/extensions/QPoll/qp_user.php"; |
31 | | - * |
32 | | - * @version 0.8.0a |
33 | | - * @link http://www.mediawiki.org/wiki/Extension:QPoll |
34 | | - * @author QuestPC <questpc@rambler.ru> |
35 | | - */ |
36 | | - |
37 | | -if ( !defined( 'MEDIAWIKI' ) ) { |
38 | | - die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
39 | | -} |
40 | | - |
41 | | -class qp_Eval { |
42 | | - |
43 | | - # the list of allowed PHP tokens |
44 | | - # filtered using the complete list at http://www.php.net/manual/ru/tokens.php |
45 | | - # is it bullet-proof enough? |
46 | | - static $allowedTokens = array( |
47 | | - T_AND_EQUAL, |
48 | | - T_ARRAY, |
49 | | - T_AS, |
50 | | - T_BOOLEAN_AND, |
51 | | - T_BOOLEAN_OR, |
52 | | - T_BOOL_CAST, |
53 | | - T_BREAK, |
54 | | - T_CASE, |
55 | | - T_COMMENT, |
56 | | - T_CONCAT_EQUAL, |
57 | | - T_CONSTANT_ENCAPSED_STRING, |
58 | | - T_CONTINUE, |
59 | | - T_DEC, |
60 | | - T_DEFAULT, |
61 | | - T_DIV_EQUAL, |
62 | | - T_DNUMBER, |
63 | | - T_DOC_COMMENT, |
64 | | - T_DOUBLE_ARROW, |
65 | | - T_DOUBLE_CAST, |
66 | | - T_ELSE, |
67 | | - T_ELSEIF, |
68 | | - T_EMPTY, |
69 | | - T_ENCAPSED_AND_WHITESPACE, |
70 | | - T_ENDFOREACH, |
71 | | - T_ENDIF, |
72 | | - T_ENDSWITCH, |
73 | | - T_END_HEREDOC, |
74 | | - T_FOREACH, |
75 | | - T_FUNCTION, |
76 | | - T_IF, |
77 | | - T_INC, |
78 | | - T_INT_CAST, |
79 | | - T_ISSET, |
80 | | - T_IS_EQUAL, |
81 | | - T_IS_GREATER_OR_EQUAL, |
82 | | - T_IS_IDENTICAL, |
83 | | - T_IS_NOT_EQUAL, |
84 | | - T_IS_NOT_IDENTICAL, |
85 | | - T_IS_SMALLER_OR_EQUAL, |
86 | | - T_LIST, |
87 | | - T_LNUMBER, |
88 | | - T_LOGICAL_AND, |
89 | | - T_LOGICAL_OR, |
90 | | - T_LOGICAL_XOR, |
91 | | - T_MINUS_EQUAL, |
92 | | - T_MOD_EQUAL, |
93 | | - T_MUL_EQUAL, |
94 | | - T_NUM_STRING, |
95 | | - T_OR_EQUAL, |
96 | | - T_PLUS_EQUAL, |
97 | | - T_RETURN, |
98 | | - T_SL, |
99 | | - T_SL_EQUAL, |
100 | | - T_SR, |
101 | | - T_SR_EQUAL, |
102 | | - T_START_HEREDOC, |
103 | | - T_STRING, |
104 | | - T_STRING_CAST, |
105 | | - T_SWITCH, |
106 | | - T_UNSET, |
107 | | - T_UNSET_CAST, |
108 | | - T_VARIABLE, |
109 | | - T_WHITESPACE, |
110 | | - T_XOR_EQUAL |
111 | | - ); |
112 | | - |
113 | | - # allowed functions |
114 | | - static $allowedCalls = array( |
115 | | - # math |
116 | | - 'round', 'ceil', 'floor', |
117 | | - # arrays |
118 | | - 'is_array', 'array_search', 'count', 'array_intersect', 'array_diff', |
119 | | - # strings |
120 | | - 'trim', 'preg_match', 'preg_match_all', 'preg_split', 'qp_lc', |
121 | | - # debug |
122 | | - 'qp_debug' |
123 | | - ); |
124 | | - |
125 | | - # disallowed superglobals |
126 | | - static $superGlobals = array( |
127 | | - '$GLOBALS', |
128 | | - '$_SERVER', |
129 | | - '$_GET', |
130 | | - '$_POST', |
131 | | - '$_FILES', |
132 | | - '$_REQUEST', |
133 | | - '$_SESSION', |
134 | | - '$_ENV', |
135 | | - '$_COOKIE', |
136 | | - '$php_errormsg', |
137 | | - '$HTTP_RAW_POST_DATA', |
138 | | - '$http_response_header', |
139 | | - '$argc', |
140 | | - '$argv' |
141 | | - ); |
142 | | - |
143 | | - # prefix added to local variable names which prevents |
144 | | - # from accessing local scope variables in eval'ed code |
145 | | - static $pseudoNamespace = 'qpv_'; |
146 | | - |
147 | | - # the list of disallowed code |
148 | | - # please add new entries, if needed. |
149 | | - # key 'badresult' means that formally the code is allowed, |
150 | | - # however the returned result has to be checked |
151 | | - # (eg. variable substitution is incorrect) |
152 | | - static $disallowedCode = array( |
153 | | - array( |
154 | | - 'code' => '$test = $_SERVER["REQUEST_URI"];', |
155 | | - 'desc' => 'Disallow reading from superglobals' |
156 | | - ), |
157 | | - array( |
158 | | - 'code' => '$GLOBALS["wgVersion"] = "test";', |
159 | | - 'desc' => 'Disallow writing to superglobals' |
160 | | - ), |
161 | | - array( |
162 | | - 'code' => 'global $wgVersion;', |
163 | | - 'desc' => 'Disallow visibility of globals in local scope' |
164 | | - ), |
165 | | - array( |
166 | | - 'code' => 'return $selfCheck == 1;', |
167 | | - 'badresult' => true, |
168 | | - 'desc' => 'Disallow access to extension\'s locals in the eval scope' |
169 | | - ), |
170 | | - array( |
171 | | - 'code' => '$writevar = 1; $var = "writevar"; $$var = "test";', |
172 | | - 'desc' => 'Disallow writing to variable variables' |
173 | | - ), |
174 | | - array( |
175 | | - 'code' => '$readvar = 1; $var = "readvar"; $test = $$var;', |
176 | | - 'desc' => 'Disallow reading from variable variables' |
177 | | - ), |
178 | | - array( |
179 | | - 'code' => '$readvar = 1; $var = "readvar"; $test = "my$$var 1";', |
180 | | - 'desc' => 'Disallow reading from complex variable variables' |
181 | | - ), |
182 | | - array( |
183 | | - 'code' => '$dh = opendir( "./" );', |
184 | | - 'desc' => 'Disallow illegal function calls' |
185 | | - ), |
186 | | - array( |
187 | | - 'code' => '$func = "opendir"; $dh=$func( "./" );', |
188 | | - 'desc' => 'Disallow variable function calls' |
189 | | - ), |
190 | | - array( |
191 | | - 'code' => 'return "test$selfCheck result";', |
192 | | - 'badresult' => 'test1 result', |
193 | | - 'desc' => 'Disallow extension\'s local scope variables in "simple" complex variables' |
194 | | - ), |
195 | | - array( |
196 | | - 'code' => '$curlydollar = "1"; $var = "test{$curlydollar}a";', |
197 | | - 'desc' => 'Disallow complex variables (curlydollar)' |
198 | | - ), |
199 | | - array( |
200 | | - 'code' => '$dollarcurly = "1"; $var = "test${dollarcurly}a";', |
201 | | - 'desc' => 'Disallow complex variables (dollarcurly)' |
202 | | - ), |
203 | | - array( |
204 | | - 'code' => '$obj = new stdClass; $obj = new stdClass(); $obj -> a = 1;', |
205 | | - 'desc' => 'Disallow creation of objects' |
206 | | - ), |
207 | | - array( |
208 | | - 'code' => '$obj -> a = 1;', |
209 | | - 'desc' => 'Disallow indirect creation of objects' |
210 | | - ), |
211 | | - array( |
212 | | - 'code' => '$obj = (object) array("a"=>1);', |
213 | | - 'desc' => 'Disallow cast to objects' |
214 | | - ), |
215 | | - array( |
216 | | - 'code' => 'for ( $i = 0; $i < 1; $i++ ) {};', |
217 | | - 'desc' => 'Disallow for loops, which easily can be made infinite' |
218 | | - ) |
219 | | - ); |
220 | | - |
221 | | - /** |
222 | | - * Calls php interpreter to lint interpretation script code |
223 | | - * @param $code string with php code |
224 | | - * @return bool true, when code has no syntax errors; |
225 | | - * string error message from php lint |
226 | | - */ |
227 | | - static function lint( $code ) { |
228 | | - $pipes = array(); |
229 | | - $spec = array( |
230 | | - 0 => array( 'pipe', 'r' ), |
231 | | - 1 => array( 'pipe', 'w' ), |
232 | | - 2 => array( 'pipe', 'w' ) |
233 | | - ); |
234 | | - if ( !function_exists( 'proc_open' ) ) { |
235 | | - return wfMsg( 'qp_error_eval_unable_to_lint' ); |
236 | | - } |
237 | | - $process = proc_open( 'php -l', $spec, $pipes ); |
238 | | - if ( !is_resource( $process ) ) { |
239 | | - return wfMsg( 'qp_error_eval_unable_to_lint' ); |
240 | | - } |
241 | | - fwrite( $pipes[0], "<?php $code" ); |
242 | | - fclose( $pipes[0] ); |
243 | | - $out = array( 1 => '', 2 => '' ); |
244 | | - foreach ( $out as $key => &$text ) { |
245 | | - while ( !feof( $pipes[$key] ) ) { |
246 | | - $text .= fgets( $pipes[$key], 1024 ); |
247 | | - } |
248 | | - fclose( $pipes[$key] ); |
249 | | - } |
250 | | - $retval = proc_close( $process ); |
251 | | - if ( $retval == 0 ) { |
252 | | - # no lint errors |
253 | | - return true; |
254 | | - } |
255 | | - if ( ( $result = trim( implode( $out ) ) ) == '' ) { |
256 | | - # lint errors but no meaningful error message |
257 | | - return wfMsg( 'qp_error_eval_unable_to_lint' ); |
258 | | - } |
259 | | - # lint error message |
260 | | - return $result; |
261 | | - } |
262 | | - |
263 | | - /** |
264 | | - * Check against the list of known disallowed code (for eval) |
265 | | - * should be executed before every eval, because PHP upgrade can introduce |
266 | | - * incompatibility leading to secure hole at any time |
267 | | - * @return |
268 | | - */ |
269 | | - static function selfCheck() { |
270 | | - # remove unavailable functions from allowed calls list |
271 | | - foreach ( self::$allowedCalls as $key => $fname ) { |
272 | | - if ( !function_exists( $fname ) ) { |
273 | | - unset( self::$allowedCalls[$key] ); |
274 | | - } |
275 | | - } |
276 | | - # the following var is used to check access to extension's locals |
277 | | - # in the eval scope |
278 | | - $selfCheck = 1; |
279 | | - foreach ( self::$disallowedCode as $key => &$sourceCode ) { |
280 | | - # check source code sample |
281 | | - $destinationCode = ''; |
282 | | - $result = self::checkAndTransformCode( $sourceCode['code'], $destinationCode ); |
283 | | - if ( isset( $sourceCode['badresult'] ) ) { |
284 | | - # the code is meant to be vaild, however the result may be insecure |
285 | | - if ( $result !== true ) { |
286 | | - # there is an error in sample |
287 | | - return 'Sample error:' . $sourceCode['desc']; |
288 | | - } |
289 | | - # suppres PHP notices because some tests are supposed to generate them |
290 | | - $old_reporting = error_reporting( E_ALL & ~E_NOTICE ); |
291 | | - $test_result = eval( $destinationCode ); |
292 | | - error_reporting( $old_reporting ); |
293 | | - # compare eval() result with "insecure" bad result |
294 | | - if ( $test_result === $sourceCode['badresult'] ) { |
295 | | - return $sourceCode['desc']; |
296 | | - } |
297 | | - } else { |
298 | | - # the code meant to be invalid |
299 | | - if ( $result === true ) { |
300 | | - # illegal destination code which was passed as vaild |
301 | | - return $sourceCode['desc']; |
302 | | - } |
303 | | - } |
304 | | - } |
305 | | - return true; |
306 | | - } |
307 | | - |
308 | | - /** |
309 | | - * Checks the submitted eval code for errors |
310 | | - * In case of success returns transformed code, which is safer for eval |
311 | | - * @param $sourceCode submitted code which has to be eval'ed (no php tags) |
312 | | - * @param $destinationCode transformed code (in case of success) (no php tags) |
313 | | - * @return boolean true in case of success, string with error message on failure |
314 | | - */ |
315 | | - static function checkAndTransformCode( $sourceCode, &$destinationCode ) { |
316 | | - |
317 | | - # tokenizer requires php tags to parse propely, |
318 | | - # eval(), however requires not to have php tags - weird.. |
319 | | - $tokens = token_get_all( "<?php $sourceCode ?>" ); |
320 | | - /* remove <?php ?> */ |
321 | | - array_shift( $tokens ); |
322 | | - array_pop( $tokens ); |
323 | | - |
324 | | - $destinationCode = ''; |
325 | | - $prev_token = null; |
326 | | - foreach ( $tokens as $token ) { |
327 | | - if ( is_array( $token ) ) { |
328 | | - list( $token_id, $content, $line ) = $token; |
329 | | - # check against generic list of disallowed tokens |
330 | | - if ( !in_array( $token_id, self::$allowedTokens, true ) ) { |
331 | | - return wfMsg( 'qp_error_eval_illegal_token', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
332 | | - } |
333 | | - if ( $token_id == T_VARIABLE ) { |
334 | | - $prev_content = is_array( $prev_token ) ? $prev_token[1] : $prev_token; |
335 | | - preg_match( '`(\$)$`', $prev_content, $matches ); |
336 | | - # disallow variable variables |
337 | | - if ( count( $matches ) > 1 && $matches[1] == '$' ) { |
338 | | - return wfMsg( 'qp_error_eval_variable_variable_access', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
339 | | - } |
340 | | - # disallow superglobals |
341 | | - if ( in_array( $content, self::$superGlobals ) ) { |
342 | | - return wfMsg( 'qp_error_eval_illegal_superglobal', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
343 | | - } |
344 | | - # restrict variable names |
345 | | - preg_match( '`^(\$)([A-Za-z0-9_]*)$`', $content, $matches ); |
346 | | - if ( count( $matches ) != 3 ) { |
347 | | - return wfMsg( 'qp_error_eval_illegal_variable_name', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
348 | | - } |
349 | | - # correct variable names into pseudonamespace 'qpv_' |
350 | | - $content = "\$" . self::$pseudoNamespace . $matches[2]; |
351 | | - } |
352 | | - # do not count whitespace as previous token |
353 | | - if ( $token_id != T_WHITESPACE ) { |
354 | | - $prev_token = $token; |
355 | | - } |
356 | | - # concat corrected token to the destination |
357 | | - $destinationCode .= $content; |
358 | | - } else { |
359 | | - if ( $token == '(' && is_array( $prev_token ) ) { |
360 | | - list( $token_id, $content, $line ) = $prev_token; |
361 | | - # disallow variable function calls |
362 | | - if ( $token_id === T_VARIABLE ) { |
363 | | - return wfMsg( 'qp_error_eval_variable_function_call', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
364 | | - } |
365 | | - # disallow non-allowed function calls based on the list |
366 | | - if ( $token_id === T_STRING && array_search( $content, self::$allowedCalls, true ) === false ) { |
367 | | - return wfMsg( 'qp_error_eval_illegal_function_call', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
368 | | - } |
369 | | - } |
370 | | - $prev_token = $token; |
371 | | - # concat current token to the destination |
372 | | - $destinationCode .= $token; |
373 | | - } |
374 | | - } |
375 | | - |
376 | | - return true; |
377 | | - } |
378 | | - |
379 | | - /** |
380 | | - * Interpretates the answer with selected script |
381 | | - * @param $interpretScript string source code of interpretation script |
382 | | - * @param $injectVars array of PHP data to inject into interpretation script; |
383 | | - * key of element will become variable name |
384 | | - * in the interpretation script; |
385 | | - * value of element will become variable value |
386 | | - * in the interpretation script; |
387 | | - * @param $interpResult instance of qp_InterpResult class |
388 | | - * @modifies $interpResult |
389 | | - * @return array script result to check, or |
390 | | - * qp_InterpResult $interpResult (in case of error) |
391 | | - */ |
392 | | - static function interpretAnswer( $interpretScript, $injectVars, qp_InterpResult $interpResult ) { |
393 | | - # template page evaluation |
394 | | - if ( ( $check = self::selfCheck() ) !== true ) { |
395 | | - # self-check error |
396 | | - return $interpResult->setError( wfMsg( 'qp_error_eval_self_check', $check ) ); |
397 | | - } |
398 | | - $evalScript = ''; |
399 | | - if ( ( $check = self::checkAndTransformCode( $interpretScript, $evalScript ) ) !== true ) { |
400 | | - # possible malicious code |
401 | | - return $interpResult->setError( $check ); |
402 | | - } |
403 | | - # inject poll answer into the interpretation script |
404 | | - $evalInject = ''; |
405 | | - foreach ( $injectVars as $varname => $var ) { |
406 | | - $evalInject .= "\$" . self::$pseudoNamespace . "{$varname} = unserialize( base64_decode( '" . base64_encode( serialize( $var ) ) . "' ) ); "; |
407 | | - } |
408 | | - $evalScript = "{$evalInject}/* */ {$evalScript}"; |
409 | | - $result = eval( $evalScript ); |
410 | | - return $result; |
411 | | - } |
412 | | - |
413 | | -} |
Index: trunk/extensions/QPoll/i18n/qp.i18n.php |
— | — | @@ -46,6 +46,7 @@ |
47 | 47 | */ |
48 | 48 | $messages['en'] = array( |
49 | 49 | 'pollresults' => 'Results of the polls on this site', |
| 50 | + 'qpollwebinstall' => 'Installation / update of QPoll extension', |
50 | 51 | 'qp_parentheses' => '($1)', |
51 | 52 | 'qp_full_category_name' => '$1($2)', |
52 | 53 | 'qp_desc' => 'Allows creation of polls', |
Index: trunk/extensions/QPoll/clientside/qp_results.css |
— | — | @@ -1,11 +1,12 @@ |
2 | 2 | .qpoll .head {font-weight:bold; color:gray;} |
3 | | -.qpoll table.pollresults {border-collapse: collapse;} |
4 | | -.qpoll table.pollresults th {border: 1px gray solid; background-color:lightgray; padding: 3px;} |
5 | | -.qpoll table.pollresults tr.spans { color:Navy; } |
6 | | -.qpoll table.pollresults td {border: 1px gray solid; text-align: center; padding: 3px;} |
7 | | -.qpoll table.pollresults td.stats {background-color: Azure;} |
8 | | -.qpoll table.pollresults td.spaneven {background-color: Aquamarine;} |
9 | | -.qpoll table.pollresults td.spanodd {background-color: Moccasin;} |
| 3 | +.qpoll table.qdata {border-collapse: collapse;} |
| 4 | +.qpoll table.qdata th {border: 1px gray solid; background-color:lightgray; padding: 3px;} |
| 5 | +.qpoll table.qdata tr.spans { color:Navy; } |
| 6 | +.qpoll table.qdata td {border: 1px gray solid; text-align: center; padding: 3px;} |
| 7 | +.qpoll table.qdata td.stats {background-color: Azure;} |
| 8 | +.qpoll table.qdata td.spaneven {background-color: Aquamarine;} |
| 9 | +.qpoll table.qdata td.spanodd {background-color: Moccasin;} |
| 10 | +.qpoll table.qdata tr.qdatatext td { text-align: left; } |
10 | 11 | .qpoll .cat_part { background-color: Lightyellow; border: 1px solid gray; padding: 0 0.5em 0 0.5em; } |
11 | 12 | .qpoll .cat_unknown { color: white; background-color: IndianRed; border: 1px solid gray; padding: 0 0.5em 0 0.5em; } |
12 | 13 | .qpoll .interp_answer { border: 1px solid gray; padding: 0; margin: 0; color: black; background-color: lightgray; font-weight:bold; line-height:2em; } |
Index: trunk/extensions/QPoll/model/qp_questiondata.php |
— | — | @@ -0,0 +1,184 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * Poll's single question data object RAM storage |
| 10 | + * ( instances usually have short name qdata ) |
| 11 | + * |
| 12 | + * *** Please do not instantiate directly. *** |
| 13 | + * *** use qp_PollStore::newQuestionData() instead *** |
| 14 | + * |
| 15 | + */ |
| 16 | +class qp_QuestionData { |
| 17 | + |
| 18 | + // DB index (with current scheme is non-unique) |
| 19 | + var $question_id = null; |
| 20 | + // common properties |
| 21 | + var $type; |
| 22 | + var $CommonQuestion; |
| 23 | + var $Categories; |
| 24 | + var $CategorySpans; |
| 25 | + var $ProposalText; |
| 26 | + var $ProposalNames = array(); |
| 27 | + var $ProposalCategoryId; |
| 28 | + var $ProposalCategoryText; |
| 29 | + var $alreadyVoted = false; // whether the selected user already voted this question ? |
| 30 | + // statistics storage |
| 31 | + var $Votes = null; |
| 32 | + var $Percents = null; |
| 33 | + |
| 34 | + /** |
| 35 | + * Constructor |
| 36 | + * @param $argv associative array, where value of key 'from' defines creation method: |
| 37 | + * 'postdata' creates qdata from question instance parsed in tag hook handler; |
| 38 | + * 'qid' creates new empty instance to be filled with data loaded from DB; |
| 39 | + * another entries of $argv define property names and their values |
| 40 | + */ |
| 41 | + function __construct( $argv ) { |
| 42 | + if ( array_key_exists( 'from', $argv ) ) { |
| 43 | + switch ( $argv[ 'from' ] ) { |
| 44 | + case 'postdata' : |
| 45 | + $this->type = $argv[ 'type' ]; |
| 46 | + $this->CommonQuestion = $argv[ 'common_question' ]; |
| 47 | + $this->Categories = $argv[ 'categories' ]; |
| 48 | + $this->CategorySpans = $argv[ 'category_spans' ]; |
| 49 | + $this->ProposalText = $argv[ 'proposal_text' ]; |
| 50 | + $this->ProposalNames = $argv[ 'proposal_names' ]; |
| 51 | + $this->ProposalCategoryId = $argv[ 'proposal_category_id' ]; |
| 52 | + $this->ProposalCategoryText = $argv[ 'proposal_category_text' ]; |
| 53 | + return; |
| 54 | + case 'qid' : |
| 55 | + $this->question_id = $argv[ 'qid' ]; |
| 56 | + $this->type = $argv[ 'type' ]; |
| 57 | + $this->CommonQuestion = $argv[ 'common_question' ]; |
| 58 | + $this->Categories = array(); |
| 59 | + $this->CategorySpans = array(); |
| 60 | + $this->ProposalText = array(); |
| 61 | + $this->ProposalCategoryId = array(); |
| 62 | + $this->ProposalCategoryText = array(); |
| 63 | + return; |
| 64 | + } |
| 65 | + } |
| 66 | + throw new MWException( "Parameter \$argv['from'] is missing or has unsupported value in " . __METHOD__ ); |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Create appropriate view for Special:Pollresults |
| 71 | + */ |
| 72 | + function createView() { |
| 73 | + return new qp_QuestionDataResults( $this ); |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * Integrate spans into categories |
| 78 | + */ |
| 79 | + function packSpans() { |
| 80 | + if ( count( $this->CategorySpans ) > 0 ) { |
| 81 | + foreach ( $this->Categories as &$Cat ) { |
| 82 | + if ( array_key_exists( 'spanId', $Cat ) ) { |
| 83 | + $Cat['name'] = $this->CategorySpans[ $Cat['spanId'] ]['name'] . "\n" . $Cat['name']; |
| 84 | + unset( $Cat['spanId'] ); |
| 85 | + } |
| 86 | + } |
| 87 | + unset( $this->CategorySpans ); |
| 88 | + $this->CategorySpans = array(); |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Restore spans from categories |
| 94 | + */ |
| 95 | + function restoreSpans() { |
| 96 | + if ( count( $this->CategorySpans ) == 0 ) { |
| 97 | + $prevSpanName = ''; |
| 98 | + $spanId = -1; |
| 99 | + foreach ( $this->Categories as &$Cat ) { |
| 100 | + $a = explode( "\n", $Cat['name'] ); |
| 101 | + if ( count( $a ) > 1 ) { |
| 102 | + if ( $prevSpanName != $a[0] ) { |
| 103 | + $spanId++; |
| 104 | + $prevSpanName = $a[0]; |
| 105 | + $this->CategorySpans[ $spanId ]['count'] = 0; |
| 106 | + } |
| 107 | + $Cat['name'] = $a[1]; |
| 108 | + $Cat['spanId'] = $spanId; |
| 109 | + $this->CategorySpans[ $spanId ]['name'] = $a[0]; |
| 110 | + $this->CategorySpans[ $spanId ]['count']++; |
| 111 | + } else { |
| 112 | + $prevSpanName = ''; |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * Check whether the previousely stored poll header is |
| 120 | + * compatible with the one defined on the page. |
| 121 | + * |
| 122 | + * Used to reject previous vote in case the header is incompatble. |
| 123 | + */ |
| 124 | + function isCompatible( &$question ) { |
| 125 | + if ( $question->mType != $this->type ) { |
| 126 | + return false; |
| 127 | + } |
| 128 | + if ( count( $question->mCategorySpans ) != count( $this->CategorySpans ) ) { |
| 129 | + return false; |
| 130 | + } |
| 131 | + foreach ( $question->mCategorySpans as $spanidx => &$span ) { |
| 132 | + if ( !isset( $this->CategorySpans[$spanidx] ) || |
| 133 | + $span['count'] != $this->CategorySpans[$spanidx]['count'] ) { |
| 134 | + return false; |
| 135 | + } |
| 136 | + } |
| 137 | + return true; |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Split raw proposal text from source page text or from DB |
| 142 | + * into name part / text part |
| 143 | + * |
| 144 | + * @param $proposal_text string raw proposal text |
| 145 | + * @modifies $proposal_text string proposal text to display |
| 146 | + * @return string proposal name or '' when there is no name |
| 147 | + */ |
| 148 | + static function splitRawProposal( &$proposal_text ) { |
| 149 | + $matches = array(); |
| 150 | + $prop_name = ''; |
| 151 | + preg_match( '`^:\|(.+?)\|\s*(.+?)$`u', $proposal_text, $matches ); |
| 152 | + if ( count( $matches ) > 2 ) { |
| 153 | + if ( ( $prop_name = trim( $matches[1] ) ) !== '' ) { |
| 154 | + # proposal name must be non-empty |
| 155 | + $proposal_text = trim( $matches[2] ); |
| 156 | + } |
| 157 | + } |
| 158 | + return $prop_name; |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * Return proposal name prefix to be stored in DB (if any) |
| 163 | + */ |
| 164 | + static function getProposalNamePrefix( $name ) { |
| 165 | + return ( $name !== '' ) ? ":|{$name}|" : ''; |
| 166 | + } |
| 167 | + |
| 168 | +} /* end of qp_QuestionData class */ |
| 169 | + |
| 170 | +/** |
| 171 | + * |
| 172 | + * *** Please do not instantiate directly. *** |
| 173 | + * *** use qp_PollStore::newQuestionData() instead *** |
| 174 | + * |
| 175 | + */ |
| 176 | +class qp_TextQuestionData extends qp_QuestionData { |
| 177 | + |
| 178 | + /** |
| 179 | + * Questions of type="text" require a different view logic in Special:Pollresults page |
| 180 | + */ |
| 181 | + function createView() { |
| 182 | + return new qp_TextQuestionDataResults( $this ); |
| 183 | + } |
| 184 | + |
| 185 | +} /* end of qp_TextQuestionData class */ |
Property changes on: trunk/extensions/QPoll/model/qp_questiondata.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 186 | + native |
Index: trunk/extensions/QPoll/model/qp_question_collection.php |
— | — | @@ -0,0 +1,192 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of QPoll. |
| 6 | + * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
| 7 | + * |
| 8 | + * QPoll is free software; you can redistribute it and/or modify |
| 9 | + * it under the terms of the GNU General Public License as published by |
| 10 | + * the Free Software Foundation; either version 2 of the License, or |
| 11 | + * (at your option) any later version. |
| 12 | + * |
| 13 | + * QPoll is distributed in the hope that it will be useful, |
| 14 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | + * GNU General Public License for more details. |
| 17 | + * |
| 18 | + * You should have received a copy of the GNU General Public License |
| 19 | + * along with QPoll; if not, write to the Free Software |
| 20 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 21 | + * |
| 22 | + * ***** END LICENSE BLOCK ***** |
| 23 | + * |
| 24 | + * QPoll is a poll tool for MediaWiki. |
| 25 | + * |
| 26 | + * To activate this extension : |
| 27 | + * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
| 28 | + * * Place the files from the extension archive there. |
| 29 | + * * Add this line at the end of your LocalSettings.php file : |
| 30 | + * require_once "$IP/extensions/QPoll/qp_user.php"; |
| 31 | + * |
| 32 | + * @version 0.8.0a |
| 33 | + * @link http://www.mediawiki.org/wiki/Extension:QPoll |
| 34 | + * @author QuestPC <questpc@rambler.ru> |
| 35 | + */ |
| 36 | + |
| 37 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * Contains the iterable collection of questions with possible randomization |
| 43 | + * (optional selection of some random questions from the whole set) |
| 44 | + */ |
| 45 | +class qp_QuestionCollection { |
| 46 | + |
| 47 | + /** |
| 48 | + * Note: |
| 49 | + * We assume that $questions and $usedQuestions do not have sparce keys |
| 50 | + * I was using internal indexes but each() was buggy and evil even in PHP 5.3.x |
| 51 | + */ |
| 52 | + |
| 53 | + # array of question objects associated with current poll |
| 54 | + private $questions = array(); |
| 55 | + # current questions key, starting from 1 |
| 56 | + private $qKey; |
| 57 | + # array of $this->questions[] indexes for question iterator (used by randomizer) |
| 58 | + private $usedQuestions = false; |
| 59 | + # current usedQuestions key, starting from 0 |
| 60 | + private $usedKey; |
| 61 | + |
| 62 | + /** |
| 63 | + * From http://php.net/manual/en/function.mt-rand.php |
| 64 | + * function returns a random integer between min and max, just like function rand() does. |
| 65 | + * Difference to rand is that the random generated number will not use any of the values |
| 66 | + * placed in $except. ($except must therefore be an array) |
| 67 | + * function returns false if $except holds all values between $min and $max. |
| 68 | + */ |
| 69 | + function rand_except( $min, $max, $except ) { |
| 70 | + # first sort array values |
| 71 | + sort( $except, SORT_NUMERIC ); |
| 72 | + # calculate average gap between except-values |
| 73 | + $except_count = count( $except ); |
| 74 | + $avg_gap = ( $max - $min + 1 - $except_count ) / ( $except_count + 1 ); |
| 75 | + if ( $avg_gap <= 0 ) { |
| 76 | + return false; |
| 77 | + } |
| 78 | + # now add min and max to $except, so all gaps between $except-values can be calculated |
| 79 | + array_unshift( $except, $min - 1 ); |
| 80 | + array_push( $except, $max + 1 ); |
| 81 | + $except_count += 2; |
| 82 | + # iterate through all values of except. If gap between 2 values is higher than average gap, |
| 83 | + # create random in this gap |
| 84 | + for ( $i = 1; $i < $except_count; $i++ ) { |
| 85 | + if ( $except[$i] - $except[$i - 1] - 1 >= $avg_gap ) { |
| 86 | + return mt_rand( $except[$i - 1] + 1, $except[$i] - 1 ); |
| 87 | + } |
| 88 | + } |
| 89 | + return false; |
| 90 | + } |
| 91 | + |
| 92 | + function randomize( $randomQuestionCount ) { |
| 93 | + $questionCount = count( $this->questions ); |
| 94 | + if ( $randomQuestionCount > $questionCount ) { |
| 95 | + $randomQuestionCount = $questionCount; |
| 96 | + } |
| 97 | + $this->usedQuestions = array(); |
| 98 | + for ( $i = 0; $i < $randomQuestionCount; $i++ ) { |
| 99 | + if ( ( $r = $this->rand_except( 1, $questionCount, $this->usedQuestions ) ) === false ) { |
| 100 | + throw new MWException( 'Bug: too many random questions in ' . __METHOD__ ); |
| 101 | + } |
| 102 | + $this->usedQuestions[] = $r; |
| 103 | + } |
| 104 | + sort( $this->usedQuestions, SORT_NUMERIC ); |
| 105 | + if ( count( $this->usedQuestions ) === 0 ) { |
| 106 | + $this->usedQuestions = false; |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + function getUsedQuestions() { |
| 111 | + return $this->usedQuestions; |
| 112 | + } |
| 113 | + |
| 114 | + function setUsedQuestions( $randomQuestions ) { |
| 115 | + if ( !is_array( $randomQuestions ) ) { |
| 116 | + foreach ( $this->questions as $qidx => &$question ) { |
| 117 | + $question->usedId = $question->mQuestionId; |
| 118 | + } |
| 119 | + return; |
| 120 | + } |
| 121 | + sort( $randomQuestions, SORT_NUMERIC ); |
| 122 | + $this->usedQuestions = array(); |
| 123 | + # questions keys start from 1 |
| 124 | + $usedId = 1; |
| 125 | + foreach ( $this->questions as $qidx => &$question ) { |
| 126 | + if ( in_array( $qidx, $randomQuestions, true ) ) { |
| 127 | + # usedQuestions keys start from 0 |
| 128 | + $this->usedQuestions[] = $qidx; |
| 129 | + $question->usedId = $usedId++; |
| 130 | + } else { |
| 131 | + $question->usedId = false; |
| 132 | + } |
| 133 | + } |
| 134 | + if ( count( $this->usedQuestions ) === 0 ) { |
| 135 | + throw new MWException( 'At least one question should not be unused in ' . __METHOD__ ); |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + function add( qp_AbstractQuestion $question ) { |
| 140 | + if ( count( $this->questions ) === 0 ) { |
| 141 | + $this->questions[1] = $question; |
| 142 | + } else { |
| 143 | + $this->questions[] = $question; |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + function totalCount() { |
| 148 | + return count( $this->questions ); |
| 149 | + } |
| 150 | + |
| 151 | + function usedCount() { |
| 152 | + $used = 0; |
| 153 | + foreach ( $this->questions as &$question ) { |
| 154 | + if ( $question->usedId !== false ) { |
| 155 | + $used++; |
| 156 | + } |
| 157 | + } |
| 158 | + return $used; |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * Reset question iterator |
| 163 | + */ |
| 164 | + function reset() { |
| 165 | + $this->qKey = 1; |
| 166 | + if ( is_array( $this->usedQuestions ) ) { |
| 167 | + $this->usedKey = 0; |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * Get current question and rewind to the next question |
| 173 | + * @return instance of qp_AbstractQuestion or derivative or |
| 174 | + * boolean false - when there are no more questions left |
| 175 | + */ |
| 176 | + function iterate() { |
| 177 | + if ( is_array( $this->usedQuestions ) ) { |
| 178 | + while ( array_key_exists( $this->usedKey, $this->usedQuestions ) ) { |
| 179 | + $qidx = $this->usedQuestions[$this->usedKey++]; |
| 180 | + if ( isset( $this->questions[$qidx] ) ) { |
| 181 | + return $this->questions[$qidx]; |
| 182 | + } |
| 183 | + } |
| 184 | + return false; |
| 185 | + } |
| 186 | + if ( array_key_exists( $this->qKey, $this->questions ) ) { |
| 187 | + $question = $this->questions[$this->qKey++]; |
| 188 | + return $question; |
| 189 | + } |
| 190 | + return false; |
| 191 | + } |
| 192 | + |
| 193 | +} |
Property changes on: trunk/extensions/QPoll/model/qp_question_collection.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 194 | + native |
Index: trunk/extensions/QPoll/model/qp_pollstore.php |
— | — | @@ -0,0 +1,920 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * poll storage and retrieval using DB |
| 10 | + * one poll may contain several questions |
| 11 | + */ |
| 12 | +class qp_PollStore { |
| 13 | + |
| 14 | + static $db = null; |
| 15 | + # indicates whether random questions must be erased / regenerated when the value of |
| 16 | + # 'randomize' attribute is changed from non-zero to zero and back |
| 17 | + static $purgeRandomQuestions = false; |
| 18 | + |
| 19 | + /// DB keys |
| 20 | + var $pid = null; |
| 21 | + var $last_uid = null; |
| 22 | + |
| 23 | + # username is used for caching of setLastUser() method (which now may be called multiple times); |
| 24 | + # also used by randomizer |
| 25 | + var $username = ''; |
| 26 | + |
| 27 | + /*** common properties ***/ |
| 28 | + var $mArticleId = null; |
| 29 | + # unique id of poll, used for addressing, also with 'qp_' prefix as the fragment part of the link |
| 30 | + var $mPollId = null; |
| 31 | + # order of poll on the page |
| 32 | + var $mOrderId = null; |
| 33 | + |
| 34 | + /*** optional attributes ***/ |
| 35 | + # dependance from other poll address in the following format: "page#otherpollid" |
| 36 | + var $dependsOn = null; |
| 37 | + # NS & DBkey of Title object representing interpretation template for Special:Pollresults page |
| 38 | + var $interpNS = 0; |
| 39 | + var $interpDBkey = null; |
| 40 | + # interpretation of user answer |
| 41 | + var $interpResult; |
| 42 | + # 1..n - number of random indexes from poll's header; 0 - poll questions are not randomized |
| 43 | + # pollstore loads / saves random indexes for every user only when this property is NOT zero |
| 44 | + # which improves performance of non-randomized polls |
| 45 | + var $randomQuestionCount = null; |
| 46 | + |
| 47 | + # array of QuestionData instances (data from/to DB) |
| 48 | + var $Questions = null; |
| 49 | + # array of random indexes of Questions[] array (optional) |
| 50 | + var $randomQuestions = false; |
| 51 | + |
| 52 | + # attempts of voting (passing the quiz). number of resubmits |
| 53 | + # note: resubmits are counted for syntax-correct answer (when the vote is stored), |
| 54 | + # yet the answer still might be logically incorrect (quiz is not passed / partially passed) |
| 55 | + var $attempts = 0; |
| 56 | + |
| 57 | + # poll processing state, read with getState() |
| 58 | + # |
| 59 | + # 'NA' - object just was created |
| 60 | + # |
| 61 | + # 'incomplete', self::stateIncomplete() |
| 62 | + # http post: not every proposals were answered: do not update DB |
| 63 | + # http get: this is not the post: do not update DB |
| 64 | + # |
| 65 | + # 'error', self::stateError() |
| 66 | + # http get: invalid question syntax, parse errors will cause submit button disabled |
| 67 | + # |
| 68 | + # 'complete', self::stateComplete() |
| 69 | + # check whether the poll was successfully submitted |
| 70 | + # store user vote to the DB (when the poll is fine) |
| 71 | + # |
| 72 | + var $mCompletedPostData; |
| 73 | + # true, after the poll results have been successfully stored to DB |
| 74 | + var $voteDone = false; |
| 75 | + |
| 76 | + /* $argv[ 'from' ] indicates type of construction, other elements of $argv vary according to 'from' |
| 77 | + */ |
| 78 | + function __construct( $argv = null ) { |
| 79 | + global $wgParser; |
| 80 | + if ( self::$db == null ) { |
| 81 | + self::$db = & wfGetDB( DB_MASTER ); |
| 82 | + } |
| 83 | + $this->interpResult = new qp_InterpResult(); |
| 84 | + if ( is_array( $argv ) && array_key_exists( "from", $argv ) ) { |
| 85 | + $this->Questions = Array(); |
| 86 | + $this->mCompletedPostData = 'NA'; |
| 87 | + $this->pid = null; |
| 88 | + $is_post = false; |
| 89 | + switch ( $argv[ 'from' ] ) { |
| 90 | + case 'poll_post' : |
| 91 | + $is_post = true; |
| 92 | + case 'poll_get' : |
| 93 | + if ( array_key_exists( 'title', $argv ) ) { |
| 94 | + $title = $argv[ 'title' ]; |
| 95 | + } else { |
| 96 | + $title = $wgParser->getTitle(); |
| 97 | + } |
| 98 | + $this->mArticleId = $title->getArticleID(); |
| 99 | + $this->mPollId = $argv[ 'poll_id' ]; |
| 100 | + if ( array_key_exists( 'order_id', $argv ) ) { |
| 101 | + $this->mOrderId = $argv[ 'order_id' ]; |
| 102 | + } |
| 103 | + if ( array_key_exists( 'dependance', $argv ) && |
| 104 | + $argv[ 'dependance' ] !== false ) { |
| 105 | + $this->dependsOn = $argv[ 'dependance' ]; |
| 106 | + } |
| 107 | + if ( array_key_exists( 'interpretation', $argv ) ) { |
| 108 | + # (0,'') indicates that interpretation template does not exists |
| 109 | + $this->interpNS = 0; |
| 110 | + $this->interpDBkey = ''; |
| 111 | + if ( $argv['interpretation'] != '' ) { |
| 112 | + $interp = Title::newFromText( $argv['interpretation'], NS_QP_INTERPRETATION ); |
| 113 | + if ( $interp instanceof Title ) { |
| 114 | + $this->interpNS = $interp->getNamespace(); |
| 115 | + $this->interpDBkey = $interp->getDBkey(); |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + if ( array_key_exists( 'randomQuestionCount', $argv ) ) { |
| 120 | + $this->randomQuestionCount = $argv['randomQuestionCount']; |
| 121 | + } |
| 122 | + # do not load / create the poll when article id is unavailable |
| 123 | + # (only during newly created page submission) |
| 124 | + if ( $this->mArticleId != 0 ) { |
| 125 | + if ( $is_post ) { |
| 126 | + $this->setPid(); |
| 127 | + } else { |
| 128 | + $this->loadPid(); |
| 129 | + if ( is_null( $this->pid ) ) { |
| 130 | + # try to create poll description (DB state was incomplete) |
| 131 | + $this->setPid(); |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + break; |
| 136 | + case 'pid' : |
| 137 | + if ( array_key_exists( 'pid', $argv ) ) { |
| 138 | + $pid = intval( $argv[ 'pid' ] ); |
| 139 | + $res = self::$db->select( 'qp_poll_desc', |
| 140 | + array( 'article_id', 'poll_id', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
| 141 | + array( 'pid' => $pid ), |
| 142 | + __METHOD__ . ":create from pid" ); |
| 143 | + $row = self::$db->fetchObject( $res ); |
| 144 | + if ( $row === false ) { |
| 145 | + throw new MWException( 'Attempt to create poll from non-existent poll id in ' . __METHOD__ ); |
| 146 | + } |
| 147 | + $this->pid = $pid; |
| 148 | + $this->mArticleId = $row->article_id; |
| 149 | + $this->mPollId = $row->poll_id; |
| 150 | + $this->mOrderId = $row->order_id; |
| 151 | + $this->dependsOn = $row->dependance; |
| 152 | + $this->interpNS = $row->interpretation_namespace; |
| 153 | + $this->interpDBkey = $row->interpretation_title; |
| 154 | + $this->randomQuestionCount = $row->random_question_count; |
| 155 | + } |
| 156 | + break; |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + // special version of constructor that builds pollstore from the given poll address |
| 162 | + // @return instance of qp_PollStore on success, false on error |
| 163 | + static function newFromAddr( $pollAddr ) { |
| 164 | + # build poll object from given poll address in args[0] |
| 165 | + $pollAddr = qp_AbstractPoll::getPrefixedPollAddress( $pollAddr ); |
| 166 | + if ( is_array( $pollAddr ) ) { |
| 167 | + list( $pollTitleStr, $pollId ) = $pollAddr; |
| 168 | + $pollTitle = Title::newFromURL( $pollTitleStr ); |
| 169 | + if ( $pollTitle !== null ) { |
| 170 | + $pollArticleId = intval( $pollTitle->getArticleID() ); |
| 171 | + if ( $pollArticleId > 0 ) { |
| 172 | + return new qp_PollStore( array( |
| 173 | + 'from' => 'poll_get', |
| 174 | + 'title' => $pollTitle, |
| 175 | + 'poll_id' => $pollId ) ); |
| 176 | + } else { |
| 177 | + return qp_Setup::ERROR_MISSED_TITLE; |
| 178 | + } |
| 179 | + } else { |
| 180 | + return qp_Setup::ERROR_MISSED_TITLE; |
| 181 | + } |
| 182 | + } else { |
| 183 | + return qp_Setup::ERROR_INVALID_ADDRESS; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * qdata instantiator (factory) |
| 189 | + * Please use it instead of qdata constructors |
| 190 | + */ |
| 191 | + static function newQuestionData( $argv ) { |
| 192 | + switch ( $argv['type'] ) { |
| 193 | + case 'textQuestion' : |
| 194 | + return new qp_TextQuestionData( $argv ); |
| 195 | + case 'singleChoice' : |
| 196 | + case 'multipleChoice' : |
| 197 | + case 'mixedChoice' : |
| 198 | + return new qp_QuestionData( $argv ); |
| 199 | + default : |
| 200 | + throw new MWException( 'Unknown type of question ' . qp_Setup::specialchars( $argv['type'] ) . ' in ' . __METHOD__ ); |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + function getPollId() { |
| 205 | + return $this->mPollId; |
| 206 | + } |
| 207 | + |
| 208 | + # returns Title object, to get a URI path, use Title::getFullText()/getPrefixedText() on it |
| 209 | + function getTitle() { |
| 210 | + if ( $this->mArticleId === 0 ) { |
| 211 | + throw new MWException( __METHOD__ . ' cannot be called for unsaved new pages' ); |
| 212 | + } |
| 213 | + if ( is_null( $this->mArticleId ) ) { |
| 214 | + throw new MWException( 'Unknown article id in ' . __METHOD__ ); |
| 215 | + } |
| 216 | + if ( is_null( $this->mPollId ) ) { |
| 217 | + throw new MWException( 'Unknown poll id in ' . __METHOD__ ); |
| 218 | + } |
| 219 | + $res = Title::newFromID( $this->mArticleId ); |
| 220 | + $res->setFragment( qp_AbstractPoll::s_getPollTitleFragment( $this->mPollId ) ); |
| 221 | + if ( !( $res instanceof Title ) ) { |
| 222 | + throw new MWException( 'Invalid title created in ' . __METHOD__ ); |
| 223 | + } |
| 224 | + return $res; |
| 225 | + } |
| 226 | + |
| 227 | + /** |
| 228 | + * @return Title instance of interpretation template |
| 229 | + */ |
| 230 | + function getInterpTitle() { |
| 231 | + $title = Title::newFromText( $this->interpDBkey, $this->interpNS ); |
| 232 | + return ( $title instanceof Title ) ? $title : null; |
| 233 | + } |
| 234 | + |
| 235 | + // warning: will work only after successful loadUserAlreadyVoted() or loadUserVote() |
| 236 | + function isAlreadyVoted() { |
| 237 | + if ( is_array( $this->Questions ) && count( $this->Questions > 0 ) ) { |
| 238 | + foreach ( $this->Questions as &$qdata ) { |
| 239 | + if ( $qdata->alreadyVoted ) |
| 240 | + return true; |
| 241 | + } |
| 242 | + } |
| 243 | + return false; |
| 244 | + } |
| 245 | + |
| 246 | + # checks whether the question with specified id exists in the poll store |
| 247 | + # @return boolean, true when the question exists |
| 248 | + function questionExists( $question_id ) { |
| 249 | + return array_key_exists( $question_id, $this->Questions ); |
| 250 | + } |
| 251 | + |
| 252 | + # load questions for the newly created poll (if the poll was voted at least once) |
| 253 | + # @return boolean, true when the questions are available, false otherwise (poll was never voted) |
| 254 | + function loadQuestions() { |
| 255 | + $result = false; |
| 256 | + $typeFromVer0_5 = array( |
| 257 | + "singleChoicePoll" => "singleChoice", |
| 258 | + "multipleChoicePoll" => "multipleChoice", |
| 259 | + "mixedChoicePoll" => "mixedChoice" |
| 260 | + ); |
| 261 | + if ( $this->pid !== null ) { |
| 262 | + $res = self::$db->select( 'qp_question_desc', |
| 263 | + array( 'question_id', 'type', 'common_question' ), |
| 264 | + array( 'pid' => $this->pid ), |
| 265 | + __METHOD__ ); |
| 266 | + if ( self::$db->numRows( $res ) > 0 ) { |
| 267 | + $result = true; |
| 268 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 269 | + $question_id = intval( $row->question_id ); |
| 270 | + # convert old (v0.5) question type string to the "new" type string |
| 271 | + if ( isset( $typeFromVer0_5[$row->type] ) ) { |
| 272 | + $row->type = $typeFromVer0_5[$row->type]; |
| 273 | + } |
| 274 | + # create a qp_QuestionData object from DB fields |
| 275 | + $this->Questions[ $question_id ] = self::newQuestionData( array( |
| 276 | + 'from' => 'qid', |
| 277 | + 'qid' => $question_id, |
| 278 | + 'type' => $row->type, |
| 279 | + 'common_question' => $row->common_question ) ); |
| 280 | + } |
| 281 | + $this->getCategories(); |
| 282 | + $this->getProposalText(); |
| 283 | + } |
| 284 | + } |
| 285 | + return $result; |
| 286 | + } |
| 287 | + |
| 288 | + /** |
| 289 | + * iterates through the list of users who voted the current poll |
| 290 | + * @return mixed false on failure, array of (uid=>username) on success (might be empty) |
| 291 | + */ |
| 292 | + function pollVotersPager( $offset = 0, $limit = 20 ) { |
| 293 | + if ( $this->pid === null ) { |
| 294 | + return false; |
| 295 | + } |
| 296 | + $qp_users_polls = self::$db->tableName( 'qp_users_polls' ); |
| 297 | + $qp_users = self::$db->tableName( 'qp_users' ); |
| 298 | + $query = "SELECT qup.uid AS uid, name AS username " . |
| 299 | + "FROM $qp_users_polls qup " . |
| 300 | + "INNER JOIN $qp_users qu ON qup.uid = qu.uid " . |
| 301 | + "WHERE pid = " . intval( $this->pid ) . " " . |
| 302 | + "LIMIT " . intval( $offset ) . ", " . intval( $limit ); |
| 303 | + $res = self::$db->query( $query, __METHOD__ ); |
| 304 | + $result = array(); |
| 305 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 306 | + $result[intval( $row->uid )] = $row->username; |
| 307 | + } |
| 308 | + return $result; |
| 309 | + } |
| 310 | + |
| 311 | + /** |
| 312 | + * returns voices of the selected users in the selected question of current poll |
| 313 | + * @param $uids array of user id's in DB |
| 314 | + * @return mixed array [uid][proposal_id][cat_id]=text_answer on success, |
| 315 | + * false on failure |
| 316 | + */ |
| 317 | + function questionVoicesRange( $question_id, array $uids ) { |
| 318 | + if ( $this->pid === null ) { |
| 319 | + return false; |
| 320 | + } |
| 321 | + $qp_question_answers = self::$db->tableName( 'qp_question_answers' ); |
| 322 | + $query = "SELECT uid, proposal_id, cat_id, text_answer " . |
| 323 | + "FROM $qp_question_answers " . |
| 324 | + "WHERE pid = " . intval( $this->pid ) . " AND question_id = " . intval( $question_id ) . " AND uid IN (" . implode( ',', array_map( 'intval', $uids ) ) . ") " . |
| 325 | + "ORDER BY uid"; |
| 326 | + $res = self::$db->query( $query, __METHOD__ ); |
| 327 | + $result = array(); |
| 328 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 329 | + $uid = intval( $row->uid ); |
| 330 | + if ( !isset( $result[$uid] ) ) { |
| 331 | + $result[$uid] = array(); |
| 332 | + } |
| 333 | + $proposal_id = intval( $row->proposal_id ); |
| 334 | + if ( !isset( $result[$uid][$proposal_id] ) ) { |
| 335 | + $result[$uid][$proposal_id] = array(); |
| 336 | + } |
| 337 | + $result[$uid][$proposal_id][intval( $row->cat_id )] = ( ( $row->text_answer == "" ) ? "+" : $row->text_answer ); |
| 338 | + } |
| 339 | + return $result; |
| 340 | + } |
| 341 | + |
| 342 | + // checks whether single user already voted the poll's questions |
| 343 | + // will be written into self::Questions[]->alreadyVoted |
| 344 | + // may be used only after loadQuestions() |
| 345 | + // returns true when the user voted to any of the currently defined questions, false otherwise |
| 346 | + function loadUserAlreadyVoted() { |
| 347 | + $result = false; |
| 348 | + if ( $this->pid === null || $this->last_uid === null || |
| 349 | + !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) { |
| 350 | + return false; |
| 351 | + } |
| 352 | + $res = self::$db->select( 'qp_question_answers', |
| 353 | + array( 'DISTINCT question_id' ), |
| 354 | + array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
| 355 | + __METHOD__ . ':load one user poll questions alreadyVoted values' ); |
| 356 | + if ( self::$db->numRows( $res ) == 0 ) { |
| 357 | + return false; |
| 358 | + } |
| 359 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 360 | + $question_id = intval( $row->question_id ); |
| 361 | + if ( $this->questionExists( $question_id ) ) { |
| 362 | + $result = $this->Questions[ $question_id ]->alreadyVoted = true; |
| 363 | + } |
| 364 | + } |
| 365 | + return $result; |
| 366 | + } |
| 367 | + |
| 368 | + // load single user vote |
| 369 | + // also loads short & long answer interpretation, when available |
| 370 | + // will be written into self::Questions[]->ProposalCategoryId,ProposalCategoryText,alreadyVoted |
| 371 | + // may be used only after loadQuestions() |
| 372 | + // returns true when any of currently defined questions has the votes, false otherwise |
| 373 | + function loadUserVote() { |
| 374 | + $result = false; |
| 375 | + if ( $this->pid === null || $this->last_uid === null || |
| 376 | + !is_array( $this->Questions ) || count( $this->Questions ) == 0 ) { |
| 377 | + return false; |
| 378 | + } |
| 379 | + $res = self::$db->select( 'qp_question_answers', |
| 380 | + array( 'question_id', 'proposal_id', 'cat_id', 'text_answer' ), |
| 381 | + array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
| 382 | + __METHOD__ . ':load one user single poll vote' ); |
| 383 | + if ( self::$db->numRows( $res ) == 0 ) { |
| 384 | + return false; |
| 385 | + } |
| 386 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 387 | + $question_id = intval( $row->question_id ); |
| 388 | + if ( $this->questionExists( $question_id ) ) { |
| 389 | + $qdata = &$this->Questions[ $question_id ]; |
| 390 | + $result = $qdata->alreadyVoted = true; |
| 391 | + $qdata->ProposalCategoryId[ intval( $row->proposal_id ) ][] = intval( $row->cat_id ); |
| 392 | + $qdata->ProposalCategoryText[ intval( $row->proposal_id ) ][] = $row->text_answer; |
| 393 | + } |
| 394 | + } |
| 395 | + return $result; |
| 396 | + } |
| 397 | + |
| 398 | + // load voting statistics (totals) from DB |
| 399 | + // input: $questions_set is optional array of integer question_id values of the current poll |
| 400 | + // output: $this->Questions[]Votes[] is set on success |
| 401 | + function loadTotals( $questions_set = false ) { |
| 402 | + if ( $this->pid !== null && |
| 403 | + is_array( $this->Questions ) && count( $this->Questions > 0 ) ) { |
| 404 | + $where = 'pid=' . self::$db->addQuotes( $this->pid ); |
| 405 | + if ( is_array( $questions_set ) ) { |
| 406 | + $where .= ' AND question_id IN ('; |
| 407 | + $first_elem = true; |
| 408 | + foreach ( $questions_set as &$qid ) { |
| 409 | + if ( $first_elem ) { |
| 410 | + $first_elem = false; |
| 411 | + } else { |
| 412 | + $where .= ','; |
| 413 | + } |
| 414 | + $where .= self::$db->addQuotes( $qid ); |
| 415 | + } |
| 416 | + $where .= ')'; |
| 417 | + } |
| 418 | + $res = self::$db->select( 'qp_question_answers', |
| 419 | + array( 'count(uid)', 'question_id', 'proposal_id', 'cat_id' ), |
| 420 | + $where, |
| 421 | + __METHOD__ . ':load single poll count of user votes', |
| 422 | + array( 'GROUP BY' => 'question_id,proposal_id,cat_id' ) ); |
| 423 | + while ( $row = self::$db->fetchRow( $res ) ) { |
| 424 | + $question_id = intval( $row[ "question_id" ] ); |
| 425 | + $propkey = intval( $row[ "proposal_id" ] ); |
| 426 | + $catkey = intval( $row[ "cat_id" ] ); |
| 427 | + if ( $this->questionExists( $question_id ) ) { |
| 428 | + $qdata = &$this->Questions[ $question_id ]; |
| 429 | + if ( !is_array( $qdata->Votes ) ) { |
| 430 | + $qdata->Votes = Array(); |
| 431 | + } |
| 432 | + if ( !array_key_exists( $propkey, $qdata->Votes ) ) { |
| 433 | + $qdata->Votes[ $propkey ] = array_fill( 0, count( $qdata->Categories ), 0 ); |
| 434 | + } |
| 435 | + $qdata->Votes[ $propkey ][ $catkey ] = intval( $row[ "count(uid)" ] ); |
| 436 | + } |
| 437 | + } |
| 438 | + } |
| 439 | + } |
| 440 | + |
| 441 | + function totalUsersAnsweredQuestion( &$qdata ) { |
| 442 | + $result = 0; |
| 443 | + if ( $this->pid !== null ) { |
| 444 | + $res = self::$db->select( 'qp_question_answers', |
| 445 | + array( 'count(distinct uid)' ), |
| 446 | + array( 'pid' => $this->pid, 'question_id' => $qdata->question_id ), |
| 447 | + __METHOD__ ); |
| 448 | + if ( $row = self::$db->fetchRow( $res ) ) { |
| 449 | + $result = intval( $row[ "count(distinct uid)" ] ); |
| 450 | + } |
| 451 | + } |
| 452 | + return $result; |
| 453 | + } |
| 454 | + |
| 455 | + // try to calculate percents for every question where Votes[] are available |
| 456 | + function calculateStatistics() { |
| 457 | + foreach ( $this->Questions as &$qdata ) { |
| 458 | + $this->calculateQuestionStatistics( $qdata ); |
| 459 | + } |
| 460 | + } |
| 461 | + |
| 462 | + // try to calculate percents for the one question |
| 463 | + private function calculateQuestionStatistics( &$qdata ) { |
| 464 | + if ( isset( $qdata->Votes ) ) { // is "votable" |
| 465 | + $qdata->restoreSpans(); |
| 466 | + $spansUsed = count( $qdata->CategorySpans ) > 0 ; |
| 467 | + foreach ( $qdata->ProposalText as $propkey => $proposal_text ) { |
| 468 | + if ( isset( $qdata->Votes[ $propkey ] ) ) { |
| 469 | + $votes_row = &$qdata->Votes[ $propkey ]; |
| 470 | + if ( $qdata->type == "singleChoice" ) { |
| 471 | + if ( $spansUsed ) { |
| 472 | + $row_totals = array_fill( 0, count( $qdata->CategorySpans ), 0 ); |
| 473 | + } else { |
| 474 | + $votes_total = 0; |
| 475 | + } |
| 476 | + foreach ( $qdata->Categories as $catkey => $cat ) { |
| 477 | + if ( isset( $votes_row[ $catkey ] ) ) { |
| 478 | + if ( $spansUsed ) { |
| 479 | + $row_totals[ intval( $cat[ "spanId" ] ) ] += $votes_row[ $catkey ]; |
| 480 | + } else { |
| 481 | + $votes_total += $votes_row[ $catkey ]; |
| 482 | + } |
| 483 | + } |
| 484 | + } |
| 485 | + } else { |
| 486 | + $votes_total = $this->totalUsersAnsweredQuestion( $qdata ); |
| 487 | + } |
| 488 | + foreach ( $qdata->Categories as $catkey => $cat ) { |
| 489 | + $num_of_votes = ''; |
| 490 | + if ( isset( $votes_row[ $catkey ] ) ) { |
| 491 | + $num_of_votes = $votes_row[ $catkey ]; |
| 492 | + if ( $spansUsed ) { |
| 493 | + if ( isset( $qdata->Categories[ $catkey ][ "spanId" ] ) ) { |
| 494 | + $votes_total = $row_totals[ intval( $qdata->Categories[ $catkey ][ "spanId" ] ) ]; |
| 495 | + } |
| 496 | + } |
| 497 | + } |
| 498 | + $qdata->Percents[ $propkey ][ $catkey ] = ( $votes_total > 0 ) ? (float) $num_of_votes / (float) $votes_total : 0.0; |
| 499 | + } |
| 500 | + } |
| 501 | + } |
| 502 | + } |
| 503 | + } |
| 504 | + |
| 505 | + private function getCategories() { |
| 506 | + $res = self::$db->select( 'qp_question_categories', |
| 507 | + array( 'question_id', 'cat_id', 'cat_name' ), |
| 508 | + array( 'pid' => $this->pid ), |
| 509 | + __METHOD__ ); |
| 510 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 511 | + $question_id = intval( $row->question_id ); |
| 512 | + $cat_id = intval( $row->cat_id ); |
| 513 | + if ( $this->questionExists( $question_id ) ) { |
| 514 | + $qdata = &$this->Questions[ $question_id ]; |
| 515 | + $qdata->Categories[ $cat_id ][ "name" ] = $row->cat_name; |
| 516 | + } |
| 517 | + } |
| 518 | + foreach ( $this->Questions as &$qdata ) { |
| 519 | + $qdata->restoreSpans(); |
| 520 | + } |
| 521 | + } |
| 522 | + |
| 523 | + private function getProposalText() { |
| 524 | + $res = self::$db->select( 'qp_question_proposals', |
| 525 | + array( 'question_id', 'proposal_id', 'proposal_text' ), |
| 526 | + array( 'pid' => $this->pid ), |
| 527 | + __METHOD__ ); |
| 528 | + # load proposal text from DB |
| 529 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 530 | + $question_id = intval( $row->question_id ); |
| 531 | + $proposal_id = intval( $row->proposal_id ); |
| 532 | + if ( $this->questionExists( $question_id ) ) { |
| 533 | + $qdata = &$this->Questions[ $question_id ]; |
| 534 | + $prop_text = $row->proposal_text; |
| 535 | + if ( ( $prop_name = qp_QuestionData::splitRawProposal( $prop_text ) ) !== '' ) { |
| 536 | + $qdata->ProposalNames[$proposal_id] = $prop_name; |
| 537 | + } |
| 538 | + $qdata->ProposalText[$proposal_id] = $prop_text; |
| 539 | + } |
| 540 | + } |
| 541 | + } |
| 542 | + |
| 543 | + function getState() { |
| 544 | + return $this->mCompletedPostData; |
| 545 | + } |
| 546 | + |
| 547 | + function stateIncomplete() { |
| 548 | + if ( $this->mCompletedPostData == 'NA' ) { |
| 549 | + $this->mCompletedPostData = 'incomplete'; |
| 550 | + } |
| 551 | + } |
| 552 | + |
| 553 | + function stateError() { |
| 554 | + $this->mCompletedPostData = 'error'; |
| 555 | + } |
| 556 | + |
| 557 | + # check whether the poll was successfully submitted |
| 558 | + # @return boolean - result of operation |
| 559 | + function stateComplete() { |
| 560 | + # completed only when previous state was unavaibale; error state can't be completed |
| 561 | + if ( $this->mCompletedPostData == 'NA' && count( $this->Questions ) > 0 ) { |
| 562 | + $this->mCompletedPostData = 'complete'; |
| 563 | + return true; |
| 564 | + } else { |
| 565 | + return false; |
| 566 | + } |
| 567 | + } |
| 568 | + |
| 569 | + /** |
| 570 | + * Checks, whether particular question belongs to user's random seed |
| 571 | + * @param $question_id question_id from DB |
| 572 | + * @return true: question belongs to the seed; |
| 573 | + * false: question does not belong to the seed; |
| 574 | + */ |
| 575 | + function isUsedQuestion( $question_id ) { |
| 576 | + return !is_array( $this->randomQuestions ) || |
| 577 | + in_array( $question_id, $this->randomQuestions, true ); |
| 578 | + } |
| 579 | + |
| 580 | + /** |
| 581 | + * Loads $this->randomQuestions from DB for current user |
| 582 | + * Will be overriden in memory when number of random questions was changed |
| 583 | + */ |
| 584 | + function loadRandomQuestions() { |
| 585 | + if ( $this->mArticleId == 0 ) { |
| 586 | + $this->randomQuestions = false; |
| 587 | + return; |
| 588 | + } |
| 589 | + if ( is_null( $this->pid ) ) { |
| 590 | + throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
| 591 | + } |
| 592 | + if ( is_null( $this->last_uid ) ) { |
| 593 | + throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
| 594 | + } |
| 595 | + $res = self::$db->select( 'qp_random_questions', 'question_id', array( 'uid' => $this->last_uid, 'pid' => $this->pid ), __METHOD__ ); |
| 596 | + $this->randomQuestions = array(); |
| 597 | + while ( $row = self::$db->fetchObject( $res ) ) { |
| 598 | + $this->randomQuestions[] = intval( $row->question_id ); |
| 599 | + } |
| 600 | + if ( count( $this->randomQuestions ) === 0 ) { |
| 601 | + $this->randomQuestions = false; |
| 602 | + } else { |
| 603 | + sort( $this->randomQuestions, SORT_NUMERIC ); |
| 604 | + } |
| 605 | + } |
| 606 | + |
| 607 | + /** |
| 608 | + * Stores $this->randomQuestions into DB |
| 609 | + * Should be called: |
| 610 | + * when user views the page with the poll first time |
| 611 | + * when number of random questions for poll was changed |
| 612 | + */ |
| 613 | + function setRandomQuestions() { |
| 614 | + if ( $this->mArticleId == 0 ) { |
| 615 | + return; |
| 616 | + } |
| 617 | + if ( is_null( $this->pid ) ) { |
| 618 | + throw new MWException( __METHOD__ . ' cannot be called when pid was not set' ); |
| 619 | + } |
| 620 | + if ( is_null( $this->last_uid ) ) { |
| 621 | + throw new MWException( __METHOD__ . ' cannot be called when uid was not set' ); |
| 622 | + } |
| 623 | + if ( is_array( $this->randomQuestions ) ) { |
| 624 | + $data = array(); |
| 625 | + foreach ( $this->randomQuestions as $qidx ) { |
| 626 | + $data[] = array( 'pid' => $this->pid, 'uid' => $this->last_uid, 'question_id' => $qidx ); |
| 627 | + } |
| 628 | + self::$db->begin(); |
| 629 | + self::$db->delete( 'qp_random_questions', |
| 630 | + array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
| 631 | + __METHOD__ ); |
| 632 | + $res = self::$db->insert( 'qp_random_questions', |
| 633 | + $data, |
| 634 | + __METHOD__ . ':set random questions seed' ); |
| 635 | + self::$db->commit(); |
| 636 | + return; |
| 637 | + } |
| 638 | + # this->randomQuestions === false; this poll is not randomized anymore |
| 639 | + self::$db->delete( 'qp_random_questions', |
| 640 | + array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
| 641 | + __METHOD__ . ':remove question random seed' |
| 642 | + ); |
| 643 | + } |
| 644 | + |
| 645 | + function setLastUser( $username, $store_new_user_to_db = true ) { |
| 646 | + if ( $this->pid === null ) { |
| 647 | + return; |
| 648 | + } |
| 649 | + # do no query DB for the same user more than once |
| 650 | + if ( $this->username === $username ) { |
| 651 | + return; |
| 652 | + } |
| 653 | + $res = self::$db->select( 'qp_users', 'uid', array( 'name' => $username ), __METHOD__ ); |
| 654 | + $row = self::$db->fetchObject( $res ); |
| 655 | + if ( $row === false ) { |
| 656 | + if ( $store_new_user_to_db ) { |
| 657 | + self::$db->insert( 'qp_users', array( 'name' => $username ), __METHOD__ . ':UpdateUser' ); |
| 658 | + $this->last_uid = intval( self::$db->insertId() ); |
| 659 | + # set username, user was created |
| 660 | + $this->username = $username; |
| 661 | + } else { |
| 662 | + $this->last_uid = null; |
| 663 | + return; |
| 664 | + } |
| 665 | + } else { |
| 666 | + $this->last_uid = intval( $row->uid ); |
| 667 | + # set username, used was loaded |
| 668 | + $this->username = $username; |
| 669 | + } |
| 670 | + $res = self::$db->select( 'qp_users_polls', |
| 671 | + array( 'attempts', 'short_interpretation', 'long_interpretation', 'structured_interpretation' ), |
| 672 | + array( 'pid' => $this->pid, 'uid' => $this->last_uid ), |
| 673 | + __METHOD__ . ':load short & long answer interpretation' ); |
| 674 | + if ( self::$db->numRows( $res ) != 0 ) { |
| 675 | + $row = self::$db->fetchObject( $res ); |
| 676 | + $this->attempts = $row->attempts; |
| 677 | + $this->interpResult = new qp_InterpResult(); |
| 678 | + $this->interpResult->short = $row->short_interpretation; |
| 679 | + $this->interpResult->long = $row->long_interpretation; |
| 680 | + $this->interpResult->structured = $row->structured_interpretation; |
| 681 | + } |
| 682 | + $this->randomQuestions = false; |
| 683 | + if ( $this->randomQuestionCount != 0 ) { |
| 684 | + $this->loadRandomQuestions(); |
| 685 | + } |
| 686 | +// todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed |
| 687 | + } |
| 688 | + |
| 689 | + function getUserName( $uid ) { |
| 690 | + if ( $uid !== null ) { |
| 691 | + $res = self::$db->select( 'qp_users', 'name', 'uid=' . self::$db->addQuotes( intval( $uid ) ), __METHOD__ ); |
| 692 | + $row = self::$db->fetchObject( $res ); |
| 693 | + if ( $row != false ) { |
| 694 | + return $row->name; |
| 695 | + } |
| 696 | + } |
| 697 | + return false; |
| 698 | + } |
| 699 | + |
| 700 | + private function loadPid() { |
| 701 | + if ( $this->mArticleId === 0 ) { |
| 702 | + return; |
| 703 | + } |
| 704 | + $res = self::$db->select( 'qp_poll_desc', |
| 705 | + array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
| 706 | + array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId ), |
| 707 | + __METHOD__ ); |
| 708 | + $row = self::$db->fetchObject( $res ); |
| 709 | + if ( $row != false ) { |
| 710 | + $this->pid = $row->pid; |
| 711 | + # some constructors don't supply the poll attributes, get the values from DB in such case |
| 712 | + if ( $this->mOrderId === null ) { |
| 713 | + $this->mOrderId = $row->order_id; |
| 714 | + } |
| 715 | + if ( $this->dependsOn === null ) { |
| 716 | + $this->dependsOn = $row->dependance; |
| 717 | + } |
| 718 | + if ( $this->interpDBkey === null ) { |
| 719 | + $this->interpNS = $row->interpretation_namespace; |
| 720 | + $this->interpDBkey = $row->interpretation_title; |
| 721 | + } |
| 722 | + if ( is_null( $this->randomQuestionCount ) ) { |
| 723 | + $this->randomQuestionCount = $row->random_question_count; |
| 724 | + } |
| 725 | + $this->updatePollAttributes( $row ); |
| 726 | + } |
| 727 | + } |
| 728 | + |
| 729 | + private function setPid() { |
| 730 | + if ( $this->mArticleId === 0 ) { |
| 731 | + throw new MWException( 'Cannot save new poll description during new page preprocess in ' . __METHOD__ ); |
| 732 | + } |
| 733 | + $res = self::$db->select( 'qp_poll_desc', |
| 734 | + array( 'pid', 'order_id', 'dependance', 'interpretation_namespace', 'interpretation_title', 'random_question_count' ), |
| 735 | + 'article_id=' . self::$db->addQuotes( $this->mArticleId ) . ' and ' . |
| 736 | + 'poll_id=' . self::$db->addQuotes( $this->mPollId ) ); |
| 737 | + $row = self::$db->fetchObject( $res ); |
| 738 | + if ( $row == false ) { |
| 739 | + self::$db->insert( 'qp_poll_desc', |
| 740 | + array( 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ), |
| 741 | + __METHOD__ . ':update poll' ); |
| 742 | + $this->pid = self::$db->insertId(); |
| 743 | + } else { |
| 744 | + $this->pid = $row->pid; |
| 745 | + $this->updatePollAttributes( $row ); |
| 746 | + } |
| 747 | +// todo: change to "insert ... on duplicate key update ..." when last_insert_id() bugs will be fixed |
| 748 | + } |
| 749 | + |
| 750 | + private function updatePollAttributes( $row ) { |
| 751 | + self::$db->begin(); |
| 752 | + if ( $this->mOrderId != $row->order_id || |
| 753 | + $this->dependsOn != $row->dependance || |
| 754 | + $this->interpNS != $row->interpretation_namespace || |
| 755 | + $this->interpDBkey != $row->interpretation_title || |
| 756 | + $this->randomQuestionCount != $row->random_question_count ) { |
| 757 | + $res = self::$db->replace( 'qp_poll_desc', |
| 758 | + array( 'poll', 'article_poll' ), |
| 759 | + array( 'pid' => $this->pid, 'article_id' => $this->mArticleId, 'poll_id' => $this->mPollId, 'order_id' => $this->mOrderId, 'dependance' => $this->dependsOn, 'interpretation_namespace' => $this->interpNS, 'interpretation_title' => $this->interpDBkey, 'random_question_count' => $this->randomQuestionCount ), |
| 760 | + __METHOD__ . ':poll attributes update' |
| 761 | + ); |
| 762 | + } |
| 763 | + if ( $this->randomQuestionCount != $row->random_question_count && |
| 764 | + $this->randomQuestionCount == 0 && |
| 765 | + self::$purgeRandomQuestions ) { |
| 766 | + # the poll questions are not randomized anymore |
| 767 | + self::$db->delete( 'qp_random_questions', |
| 768 | + array( 'pid' => $this->pid ), |
| 769 | + __METHOD__ . ':delete unused random seeds' ); |
| 770 | + } |
| 771 | + self::$db->commit(); |
| 772 | + } |
| 773 | + |
| 774 | + private function setQuestionDesc() { |
| 775 | + $insert = array(); |
| 776 | + foreach ( $this->Questions as $qkey => &$ques ) { |
| 777 | + $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'type' => $ques->type, 'common_question' => $ques->CommonQuestion ); |
| 778 | + $ques->question_id = $qkey; |
| 779 | + } |
| 780 | + if ( count( $insert ) > 0 ) { |
| 781 | + self::$db->replace( 'qp_question_desc', |
| 782 | + array( 'question' ), |
| 783 | + $insert, |
| 784 | + __METHOD__ ); |
| 785 | + } |
| 786 | + } |
| 787 | + |
| 788 | + private function setCategories() { |
| 789 | + $insert = Array(); |
| 790 | + foreach ( $this->Questions as $qkey => &$ques ) { |
| 791 | + $ques->packSpans(); |
| 792 | + foreach ( $ques->Categories as $catkey => &$Cat ) { |
| 793 | + $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'cat_id' => $catkey, 'cat_name' => $Cat["name"] ); |
| 794 | + } |
| 795 | + $ques->restoreSpans(); |
| 796 | + } |
| 797 | + if ( count( $insert ) > 0 ) { |
| 798 | + self::$db->replace( 'qp_question_categories', |
| 799 | + array( 'category' ), |
| 800 | + $insert, |
| 801 | + __METHOD__ ); |
| 802 | + } |
| 803 | + } |
| 804 | + |
| 805 | + private function setProposals() { |
| 806 | + global $wgContLang; |
| 807 | + $insert = Array(); |
| 808 | + foreach ( $this->Questions as $qkey => &$ques ) { |
| 809 | + foreach ( $ques->ProposalText as $propkey => $ptext ) { |
| 810 | + if ( isset( $ques->ProposalNames[$propkey] ) ) { |
| 811 | + $ptext = qp_QuestionData::getProposalNamePrefix( $ques->ProposalNames[$propkey] ) . $ptext; |
| 812 | + } |
| 813 | + $insert[] = array( 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'proposal_text' => $wgContLang->truncate( $ptext, qp_Setup::$proposal_max_length , '' ) ); |
| 814 | + } |
| 815 | + } |
| 816 | + if ( count( $insert ) > 0 ) { |
| 817 | + self::$db->replace( 'qp_question_proposals', |
| 818 | + array( 'proposal' ), |
| 819 | + $insert, |
| 820 | + __METHOD__ ); |
| 821 | + } |
| 822 | + } |
| 823 | + |
| 824 | + /** |
| 825 | + * Prepares an array of user answer to the current poll and interprets these |
| 826 | + * Stores the result in $this->interpResult |
| 827 | + */ |
| 828 | + private function interpretVote() { |
| 829 | + $this->interpResult = new qp_InterpResult(); |
| 830 | + $interpTitle = $this->getInterpTitle(); |
| 831 | + if ( $interpTitle === null ) { |
| 832 | + return; |
| 833 | + } |
| 834 | + $interpArticle = new Article( $interpTitle, 0 ); |
| 835 | + if ( !$interpArticle->exists() ) { |
| 836 | + return; |
| 837 | + } |
| 838 | + |
| 839 | + # prepare array of user answers that will be passed to the interpreter |
| 840 | + $poll_answer = array(); |
| 841 | + |
| 842 | + foreach ( $this->Questions as &$qdata ) { |
| 843 | + if ( !$this->isUsedQuestion( $qdata->question_id ) ) { |
| 844 | + continue; |
| 845 | + } |
| 846 | + $questions = array(); |
| 847 | + foreach ( $qdata->ProposalText as $propkey => &$proposal_text ) { |
| 848 | + $proposals = array(); |
| 849 | + foreach ( $qdata->Categories as $catkey => &$cat_name ) { |
| 850 | + $text_answer = ''; |
| 851 | + if ( array_key_exists( $propkey, $qdata->ProposalCategoryId ) && |
| 852 | + ( $id_key = array_search( $catkey, $qdata->ProposalCategoryId[ $propkey ] ) ) !== false ) { |
| 853 | + $proposals[$catkey] = $qdata->ProposalCategoryText[ $propkey ][ $id_key ]; |
| 854 | + } |
| 855 | + } |
| 856 | + if ( isset( $qdata->ProposalNames[$propkey] ) ) { |
| 857 | + $questions[$qdata->ProposalNames[$propkey]] = $proposals; |
| 858 | + } else { |
| 859 | + $questions[$propkey] = $proposals; |
| 860 | + } |
| 861 | + } |
| 862 | + $poll_answer[$qdata->question_id] = $questions; |
| 863 | + } |
| 864 | + |
| 865 | + # interpret the poll answer to get interpretation answer |
| 866 | + $this->interpResult = qp_Interpret::getResult( $interpArticle, array( 'answer' => $poll_answer, 'randomQuestions' => $this->randomQuestions ) ); |
| 867 | + } |
| 868 | + |
| 869 | + // warning: requires qp_PollStorage::last_uid to be set |
| 870 | + private function setAnswers() { |
| 871 | + $insert = Array(); |
| 872 | + foreach ( $this->Questions as $qkey => &$ques ) { |
| 873 | + foreach ( $ques->ProposalCategoryId as $propkey => &$prop_answers ) { |
| 874 | + foreach ( $prop_answers as $idkey => $catkey ) { |
| 875 | + $insert[] = array( 'uid' => $this->last_uid, 'pid' => $this->pid, 'question_id' => $qkey, 'proposal_id' => $propkey, 'cat_id' => $catkey, 'text_answer' => $ques->ProposalCategoryText[ $propkey ][ $idkey ] ); |
| 876 | + } |
| 877 | + } |
| 878 | + } |
| 879 | + # TODO: delete votes of all users, when the POST question header is incompatible with question header in DB ? |
| 880 | + # delete previous vote to make sure previous header of this poll was not incompatible with current vote |
| 881 | + self::$db->delete( 'qp_question_answers', |
| 882 | + array( 'uid' => $this->last_uid, 'pid' => $this->pid ), |
| 883 | + __METHOD__ . ':delete previous answers of current user to the same poll' |
| 884 | + ); |
| 885 | + # vote |
| 886 | + if ( count( $insert ) > 0 ) { |
| 887 | + self::$db->replace( 'qp_question_answers', |
| 888 | + array( 'answer' ), |
| 889 | + $insert, |
| 890 | + __METHOD__ ); |
| 891 | + # update interpretation result and number of syntax-valid resubmit attempts |
| 892 | + $qp_users_polls = self::$db->tableName( 'qp_users_polls' ); |
| 893 | + $short = self::$db->addQuotes( $this->interpResult->short ); |
| 894 | + $long = self::$db->addQuotes( $this->interpResult->long ); |
| 895 | + $structured = self::$db->addQuotes( $this->interpResult->structured ); |
| 896 | + $this->attempts++; |
| 897 | + $stmt = "INSERT INTO {$qp_users_polls} (uid,pid,short_interpretation,long_interpretation,structured_interpretation)\n VALUES ( " . intval( $this->last_uid ) . ", " . intval( $this->pid ) . ", {$short}, {$long}, {$structured} )\n ON DUPLICATE KEY UPDATE attempts = " . intval( $this->attempts ) . ", short_interpretation = {$short} , long_interpretation = {$long}, structured_interpretation = {$structured}"; |
| 898 | + self::$db->query( $stmt, __METHOD__ ); |
| 899 | + } |
| 900 | + } |
| 901 | + |
| 902 | + # when the user votes and poll wasn't previousely voted yet, it also creates the poll structures in DB |
| 903 | + function setUserVote() { |
| 904 | + if ( $this->pid !== null && |
| 905 | + $this->last_uid !== null && |
| 906 | + $this->mCompletedPostData == "complete" && |
| 907 | + is_array( $this->Questions ) && count( $this->Questions ) > 0 ) { |
| 908 | + self::$db->begin(); |
| 909 | + $this->setQuestionDesc(); |
| 910 | + $this->setCategories(); |
| 911 | + $this->setProposals(); |
| 912 | + $this->interpretVote(); |
| 913 | + if ( $this->interpResult->hasToBeStored() ) { |
| 914 | + $this->setAnswers(); |
| 915 | + } |
| 916 | + self::$db->commit(); |
| 917 | + $this->voteDone = true; |
| 918 | + } |
| 919 | + } |
| 920 | + |
| 921 | +} |
Property changes on: trunk/extensions/QPoll/model/qp_pollstore.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 922 | + native |
Index: trunk/extensions/QPoll/specials/qp_results.php |
— | — | @@ -58,7 +58,7 @@ |
59 | 59 | * @param $user User: the user to check |
60 | 60 | * @return Boolean: does the user have permission to view the page? |
61 | 61 | */ |
62 | | - public function userCanExecute( User $user ) { |
| 62 | + public function userCanExecute( $user ) { |
63 | 63 | # this fn is used to decide whether to show the page link at Special:Specialpages |
64 | 64 | foreach ( self::$accessPermissions as $permission ) { |
65 | 65 | if ( !$user->isAllowed( $permission ) ) { |
— | — | @@ -230,8 +230,9 @@ |
231 | 231 | foreach ( $pollStore->Questions as &$qdata ) { |
232 | 232 | if ( $pollStore->isUsedQuestion( $qdata->question_id ) ) { |
233 | 233 | $output .= "<br />\n<b>" . $qdata->question_id . ".</b> " . qp_Setup::entities( $qdata->CommonQuestion ) . "<br />\n"; |
234 | | - $qview = |
235 | | - $output .= $qdata->displayUserQuestionVote(); |
| 234 | + $qview = $qdata->createView(); |
| 235 | + $output .= $qview->displayUserQuestionVote(); |
| 236 | + unset( $qview ); |
236 | 237 | } |
237 | 238 | } |
238 | 239 | return $output; |
— | — | @@ -252,7 +253,9 @@ |
253 | 254 | $output .= $this->qpLink( $this->getTitle(), wfMsg( 'qp_export_to_xls' ), array( "style" => "font-weight:bold;" ), array( 'action' => 'stats_xls', 'id' => $pid ) ) . "<br />\n"; |
254 | 255 | $output .= $this->qpLink( $this->getTitle(), wfMsg( 'qp_voices_to_xls' ), array( "style" => "font-weight:bold;" ), array( 'action' => 'voices_xls', 'id' => $pid ) ) . "<br />\n"; |
255 | 256 | foreach ( $pollStore->Questions as &$qdata ) { |
256 | | - $output .= $qdata->displayQuestionStats( $this, $pid ); |
| 257 | + $qview = $qdata->createView(); |
| 258 | + $output .= $qview->displayQuestionStats( $this, $pid ); |
| 259 | + unset( $qview ); |
257 | 260 | } |
258 | 261 | } |
259 | 262 | } |
— | — | @@ -481,9 +484,11 @@ |
482 | 485 | return "<div>" . self::$PollsLink . "</div>\n"; |
483 | 486 | } |
484 | 487 | |
485 | | -} |
| 488 | +} /* end of PollResults class */ |
486 | 489 | |
487 | | -/* list of all users */ |
| 490 | +/** |
| 491 | + * List all users |
| 492 | + */ |
488 | 493 | class qp_UsersList extends qp_QueryPage { |
489 | 494 | var $cmd; |
490 | 495 | var $order_by; |
— | — | @@ -545,9 +550,11 @@ |
546 | 551 | return PollResults::getPollsLink() . '<div class="head">' . wfMsg( 'qp_users_list' ) . '<div>' . $this->different_order_by_link . '</div></div>'; |
547 | 552 | } |
548 | 553 | |
549 | | -} |
| 554 | +} /* end of qp_UsersList class */ |
550 | 555 | |
551 | | -/* list of polls in which selected user (did not|participated) */ |
| 556 | +/** |
| 557 | + * List of polls in which selected user has (not) participated |
| 558 | + */ |
552 | 559 | class qp_UserPollsList extends qp_QueryPage { |
553 | 560 | var $uid; |
554 | 561 | var $inverse; |
— | — | @@ -623,9 +630,11 @@ |
624 | 631 | return $params; |
625 | 632 | } |
626 | 633 | |
627 | | -} |
| 634 | +} /* end of qp_UserPollsList class */ |
628 | 635 | |
629 | | -/* list of all polls */ |
| 636 | +/** |
| 637 | + * List all polls |
| 638 | + */ |
630 | 639 | class qp_PollsList extends qp_QueryPage { |
631 | 640 | |
632 | 641 | function getIntervalResults( $offset, $limit ) { |
— | — | @@ -648,7 +657,7 @@ |
649 | 658 | |
650 | 659 | function formatResult( $result ) { |
651 | 660 | global $wgLang, $wgContLang; |
652 | | - $poll_title = Title::makeTitle( $result->ns, $result->title, qp_AbstractPoll::getPollTitleFragment( $result->poll_id, '' ) ); |
| 661 | + $poll_title = Title::makeTitle( $result->ns, $result->title, qp_AbstractPoll::s_getPollTitleFragment( $result->poll_id, '' ) ); |
653 | 662 | $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) ); |
654 | 663 | $pollname = qp_Setup::specialchars( $result->poll_id ); |
655 | 664 | $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) ); |
— | — | @@ -663,9 +672,11 @@ |
664 | 673 | return PollResults::getUsersLink() . '<div class="head">' . wfMsg( 'qp_polls_list' ) . '</div>'; |
665 | 674 | } |
666 | 675 | |
667 | | -} |
| 676 | +} /* end of qp_PollsList class */ |
668 | 677 | |
669 | | -/* list of users, (not|participated) in particular poll, defined by pid */ |
| 678 | +/** |
| 679 | + * List of users, (not) participated in particular poll, defined by pid |
| 680 | + */ |
670 | 681 | class qp_PollUsersList extends qp_QueryPage { |
671 | 682 | |
672 | 683 | var $pid; |
— | — | @@ -689,7 +700,7 @@ |
690 | 701 | 'page_id=article_id and pid=' . $db->addQuotes( $this->pid ), |
691 | 702 | __METHOD__ ); |
692 | 703 | if ( $row = $db->fetchObject( $res ) ) { |
693 | | - $poll_title = Title::makeTitle( intval( $row->ns ), $row->title, qp_AbstractPoll::getPollTitleFragment( $row->poll_id, '' ) ); |
| 704 | + $poll_title = Title::makeTitle( intval( $row->ns ), $row->title, qp_AbstractPoll::s_getPollTitleFragment( $row->poll_id, '' ) ); |
694 | 705 | $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) ); |
695 | 706 | $pollname = qp_Setup::specialchars( $row->poll_id ); |
696 | 707 | $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) ); |
— | — | @@ -743,9 +754,11 @@ |
744 | 755 | return $params; |
745 | 756 | } |
746 | 757 | |
747 | | -} |
| 758 | +} /* end of qp_PollUsersList class */ |
748 | 759 | |
749 | | -/* list of users who voted for particular choice of particular proposal of particular question */ |
| 760 | +/** |
| 761 | + * List of users who voted for particular choice of particular proposal of particular question |
| 762 | + */ |
750 | 763 | class qp_UserCellList extends qp_QueryPage { |
751 | 764 | var $cmd; |
752 | 765 | var $pid = null; |
— | — | @@ -782,7 +795,7 @@ |
783 | 796 | $pollStore = new qp_PollStore( array( 'from' => 'pid', 'pid' => $this->pid ) ); |
784 | 797 | if ( $pollStore->pid !== null ) { |
785 | 798 | $pollStore->loadQuestions(); |
786 | | - $poll_title = Title::makeTitle( intval( $this->ns ), $this->title, qp_AbstractPoll::getPollTitleFragment( $this->poll_id, '' ) ); |
| 799 | + $poll_title = Title::makeTitle( intval( $this->ns ), $this->title, qp_AbstractPoll::s_getPollTitleFragment( $this->poll_id, '' ) ); |
787 | 800 | $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) ); |
788 | 801 | $pollname = qp_Setup::specialchars( $this->poll_id ); |
789 | 802 | $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) ); |
— | — | @@ -865,31 +878,4 @@ |
866 | 879 | return $params; |
867 | 880 | } |
868 | 881 | |
869 | | -} |
870 | | - |
871 | | -class qp_Excel { |
872 | | - |
873 | | - static function prepareExcelString( $s ) { |
874 | | - if ( preg_match( '`^=.?`', $s ) ) { |
875 | | - return "'" . $s; |
876 | | - } |
877 | | - return $s; |
878 | | - } |
879 | | - |
880 | | - static function writeFormattedTable( $worksheet, $rownum, $colnum, &$table, $format = null ) { |
881 | | - foreach ( $table as $rnum => &$row ) { |
882 | | - foreach ( $row as $cnum => &$cell ) { |
883 | | - if ( is_array( $cell ) ) { |
884 | | - if ( array_key_exists( "format", $cell ) ) { |
885 | | - $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell[ 0 ], $cell[ "format" ] ); |
886 | | - } else { |
887 | | - $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell[ 0 ], $format ); |
888 | | - } |
889 | | - } else { |
890 | | - $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell, $format ); |
891 | | - } |
892 | | - } |
893 | | - } |
894 | | - } |
895 | | - |
896 | | -} |
| 882 | +} /* end of qp_UserCellList class */ |
Index: trunk/extensions/QPoll/specials/qp_webinstall.php |
— | — | @@ -16,11 +16,20 @@ |
17 | 17 | parent::__construct( 'QPollWebInstall', 'read' ); |
18 | 18 | } |
19 | 19 | |
| 20 | + /** |
| 21 | + * Checks if the given user (identified by an object) can execute this special page |
| 22 | + * @param $user User: the user to check |
| 23 | + * @return Boolean: does the user have permission to view the page? |
| 24 | + */ |
| 25 | + public function userCanExecute( $user ) { |
| 26 | + return count( array_intersect( $this->allowed_groups, $user->getEffectiveGroups() ) ) > 0; |
| 27 | + } |
| 28 | + |
20 | 29 | public function execute( $par ) { |
21 | 30 | global $wgOut, $wgUser; |
22 | 31 | |
23 | 32 | # only sysops and bureaucrats can update the DB |
24 | | - if ( count( array_intersect( $this->allowed_groups, $wgUser->getEffectiveGroups() ) ) == 0 ) { |
| 33 | + if ( !$this->userCanExecute( $wgUser ) ) { |
25 | 34 | $wgOut->addHTML( 'You have to be a member of the following group(s) to perform web install:' . implode( ', ', $this->allowed_groups ) ); |
26 | 35 | return; |
27 | 36 | } |
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php |
— | — | @@ -40,7 +40,8 @@ |
41 | 41 | /** |
42 | 42 | * A poll stub controller which cannot process and render itself |
43 | 43 | * to process and render itself, real Poll should extend this class to implement it's own: |
44 | | - * $this->getPollStore() |
| 44 | + * $this->setHeaders() |
| 45 | + * $this->getPollStore() |
45 | 46 | * $this->parseInput() |
46 | 47 | * $this->view->renderPoll() |
47 | 48 | */ |
— | — | @@ -106,7 +107,7 @@ |
107 | 108 | $view->setPerRow( $perRow ); |
108 | 109 | $this->view = $view; |
109 | 110 | # reset the unique index number of the question in the current poll (used to instantiate the questions) |
110 | | - $this->mQuestionId = 0; // ( correspons to 'question_id' DB field ) |
| 111 | + $this->mQuestionId = 0; // ( corresponds to 'question_id' DB field ) |
111 | 112 | $this->username = qp_Setup::getCurrUserName(); |
112 | 113 | # setup poll view showresults |
113 | 114 | if ( array_key_exists( 'showresults', $argv ) && qp_Setup::$global_showresults != 0 ) { |
— | — | @@ -134,10 +135,14 @@ |
135 | 136 | * @param $input Text between <qpoll> and </qpoll> tags, in QPoll syntax. |
136 | 137 | */ |
137 | 138 | function parsePoll( $input ) { |
138 | | - if ( ( $result = $this->getPollStore() ) !== true ) { |
| 139 | + if ( ( $result = $this->setHeaders() ) !== true ) { |
139 | 140 | # error message box (invalid poll attributes) |
140 | 141 | return $result; |
141 | 142 | } |
| 143 | + if ( ( $result = $this->getPollStore() ) !== true ) { |
| 144 | + # error message box (cannot load from store) |
| 145 | + return $result; |
| 146 | + } |
142 | 147 | if ( $this->parseInput( $input ) === true ) { |
143 | 148 | # no output generation - due to active redirect or access denied |
144 | 149 | return ''; |
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php |
— | — | @@ -47,9 +47,12 @@ |
48 | 48 | $this->pollAddr = trim( $argv['address'] ); |
49 | 49 | } |
50 | 50 | |
51 | | - # prepare qp_PollStore object |
52 | | - # @return true on success ($this->pollStore has been created successfully), error string on failure |
53 | | - function getPollStore() { |
| 51 | + /** |
| 52 | + * Set poll headers; checks poll headers for errors |
| 53 | + * @return string error message to display; |
| 54 | + * boolean true when no errors |
| 55 | + */ |
| 56 | + function setHeaders() { |
54 | 57 | if ( $this->mPollId !== null ) { |
55 | 58 | $this->mState = "error"; |
56 | 59 | return self::fatalError( 'qp_error_id_in_stats_mode' ); |
— | — | @@ -58,6 +61,15 @@ |
59 | 62 | $this->mState = "error"; |
60 | 63 | return self::fatalError( 'qp_error_dependance_in_stats_mode' ); |
61 | 64 | } |
| 65 | + return true; |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Prepares qp_PollStore object |
| 70 | + * @return boolean true on success ($this->pollStore has been created successfully) |
| 71 | + * string error message on failure |
| 72 | + */ |
| 73 | + function getPollStore() { |
62 | 74 | $this->pollStore = qp_PollStore::newFromAddr( $this->pollAddr ); |
63 | 75 | if ( !( $this->pollStore instanceof qp_PollStore ) || $this->pollStore->pid === null ) { |
64 | 76 | return self::fatalError( 'qp_error_no_such_poll', $this->pollAddr ); |
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php |
— | — | @@ -59,6 +59,9 @@ |
60 | 60 | # dependance attr |
61 | 61 | if ( array_key_exists( 'dependance', $argv ) ) { |
62 | 62 | $this->dependsOn = trim( $argv['dependance'] ); |
| 63 | + if ( $this->dependsOn === 'dependance' ) { |
| 64 | + $this->dependsOn = ''; |
| 65 | + } |
63 | 66 | } |
64 | 67 | # interpretation attr |
65 | 68 | if ( array_key_exists( 'interpretation', $argv ) ) { |
— | — | @@ -97,10 +100,12 @@ |
98 | 101 | $this->mBeingCorrected = ( $this->mRequest->getVal( 'pollId' ) == $this->mPollId ); |
99 | 102 | } |
100 | 103 | |
101 | | - # prepare qp_PollStore object |
102 | | - # @return true on success ($this->pollStore has been created successfully), error string on failure |
103 | | - function getPollStore() { |
104 | | - # check the headers for errors |
| 104 | + /** |
| 105 | + * Set poll headers; checks poll headers for errors |
| 106 | + * @return string error message to display; |
| 107 | + * boolean true when no errors |
| 108 | + */ |
| 109 | + function setHeaders() { |
105 | 110 | if ( $this->mPollId == null ) { |
106 | 111 | $this->mState = "error"; |
107 | 112 | return self::fatalError( 'qp_error_no_poll_id' ); |
— | — | @@ -120,12 +125,17 @@ |
121 | 126 | } |
122 | 127 | if ( $this->dependsOn != '' ) { |
123 | 128 | $depsOnAddr = self::getPrefixedPollAddress( $this->dependsOn ); |
124 | | - if ( is_array( $depsOnAddr ) ) { |
125 | | - $this->dependsOn = $depsOnAddr[2]; |
126 | | - } else { |
| 129 | + if ( !is_array( $depsOnAddr ) ) { |
127 | 130 | return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $this->dependsOn ); |
128 | 131 | } |
| 132 | + $this->dependsOn = $depsOnAddr[2]; |
129 | 133 | } |
| 134 | + return true; |
| 135 | + } |
| 136 | + |
| 137 | + # prepare qp_PollStore object |
| 138 | + # @return true on success ($this->pollStore has been created successfully), error string on failure |
| 139 | + function getPollStore() { |
130 | 140 | $newPollStore = array( |
131 | 141 | 'poll_id' => $this->mPollId, |
132 | 142 | 'order_id' => $this->mOrderId, |
— | — | @@ -240,66 +250,65 @@ |
241 | 251 | # @return true when dependance is fulfilled, error message otherwise |
242 | 252 | private function checkDependance( $dependsOn, $nonVotedDepLink = false ) { |
243 | 253 | # check the headers for dependance to other polls |
244 | | - if ( $dependsOn !== '' ) { |
245 | | - $depPollStore = qp_PollStore::newFromAddr( $dependsOn ); |
246 | | - if ( $depPollStore instanceof qp_PollStore ) { |
247 | | - # process recursive dependance |
248 | | - $depTitle = $depPollStore->getTitle(); |
249 | | - $depPollId = $depPollStore->mPollId; |
250 | | - $depLink = $this->view->link( $depTitle, $depTitle->getPrefixedText() . ' (' . $depPollStore->mPollId . ')' ); |
251 | | - if ( $depPollStore->pid === null ) { |
252 | | - return self::fatalError( 'qp_error_missed_dependance_poll', $this->mPollId, $depLink, $depPollId ); |
253 | | - } |
254 | | - if ( !$depPollStore->loadQuestions() ) { |
255 | | - return self::fatalError( 'qp_error_vote_dependance_poll', $depLink ); |
256 | | - } |
257 | | - $depPollStore->setLastUser( $this->username, false ); |
258 | | - if ( $depPollStore->loadUserAlreadyVoted() ) { |
259 | | - # user already voted in current the poll in chain |
260 | | - if ( $depPollStore->dependsOn === '' ) { |
261 | | - if ( $nonVotedDepLink === false ) { |
262 | | - # there was no non-voted deplinks in the chain at some previous level of recursion |
263 | | - return true; |
264 | | - } else { |
265 | | - # there is an non-voted deplink in the chain at some previous level of recursion |
266 | | - return self::fatalError( 'qp_error_vote_dependance_poll', $nonVotedDepLink ); |
267 | | - } |
| 254 | + if ( $dependsOn === '' ) { |
| 255 | + return true; |
| 256 | + } |
| 257 | + $depPollStore = qp_PollStore::newFromAddr( $dependsOn ); |
| 258 | + if ( $depPollStore instanceof qp_PollStore ) { |
| 259 | + # process recursive dependance |
| 260 | + $depTitle = $depPollStore->getTitle(); |
| 261 | + $depPollId = $depPollStore->mPollId; |
| 262 | + $depLink = $this->view->link( $depTitle, $depTitle->getPrefixedText() . ' (' . $depPollStore->mPollId . ')' ); |
| 263 | + if ( $depPollStore->pid === null ) { |
| 264 | + return self::fatalError( 'qp_error_missed_dependance_poll', $this->mPollId, $depLink, $depPollId ); |
| 265 | + } |
| 266 | + if ( !$depPollStore->loadQuestions() ) { |
| 267 | + return self::fatalError( 'qp_error_vote_dependance_poll', $depLink ); |
| 268 | + } |
| 269 | + $depPollStore->setLastUser( $this->username, false ); |
| 270 | + if ( $depPollStore->loadUserAlreadyVoted() ) { |
| 271 | + # user already voted in current the poll in chain |
| 272 | + if ( $depPollStore->dependsOn === '' ) { |
| 273 | + if ( $nonVotedDepLink === false ) { |
| 274 | + # there was no non-voted deplinks in the chain at some previous level of recursion |
| 275 | + return true; |
268 | 276 | } else { |
269 | | - return $this->checkDependance( $depPollStore->dependsOn, $nonVotedDepLink ); |
| 277 | + # there is an non-voted deplink in the chain at some previous level of recursion |
| 278 | + return self::fatalError( 'qp_error_vote_dependance_poll', $nonVotedDepLink ); |
270 | 279 | } |
271 | 280 | } else { |
272 | | - # user hasn't voted in current the poll in chain |
273 | | - if ( $depPollStore->dependsOn === '' ) { |
274 | | - # current element of chain is not voted and furthermore, doesn't depend on any other polls |
275 | | - return self::fatalError( 'qp_error_vote_dependance_poll', $depLink ); |
276 | | - } else { |
277 | | - # current element of chain is not voted, BUT it has it's own dependance |
278 | | - # so we will check for the most deeply nested poll which hasn't voted, yet |
279 | | - return $this->checkDependance( $depPollStore->dependsOn, $depLink ); |
280 | | - } |
| 281 | + return $this->checkDependance( $depPollStore->dependsOn, $nonVotedDepLink ); |
281 | 282 | } |
282 | 283 | } else { |
283 | | - # process poll address errors |
284 | | - switch ( $depPollStore ) { |
285 | | - case qp_Setup::ERROR_INVALID_ADDRESS : |
| 284 | + # user hasn't voted in current the poll in chain |
| 285 | + if ( $depPollStore->dependsOn === '' ) { |
| 286 | + # current element of chain is not voted and furthermore, doesn't depend on any other polls |
| 287 | + return self::fatalError( 'qp_error_vote_dependance_poll', $depLink ); |
| 288 | + } else { |
| 289 | + # current element of chain is not voted, BUT it has it's own dependance |
| 290 | + # so we will check for the most deeply nested poll which hasn't voted, yet |
| 291 | + return $this->checkDependance( $depPollStore->dependsOn, $depLink ); |
| 292 | + } |
| 293 | + } |
| 294 | + } else { |
| 295 | + # process poll address errors |
| 296 | + switch ( $depPollStore ) { |
| 297 | + case qp_Setup::ERROR_INVALID_ADDRESS : |
| 298 | + return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn ); |
| 299 | + case qp_Setup::ERROR_MISSED_TITLE : |
| 300 | + $depSplit = self::getPrefixedPollAddress( $dependsOn ); |
| 301 | + if ( is_array( $depSplit ) ) { |
| 302 | + list( $depTitleStr, $depPollId ) = $depSplit; |
| 303 | + $depTitle = Title::newFromURL( $depTitleStr ); |
| 304 | + $depTitleStr = $depTitle->getPrefixedText(); |
| 305 | + $depLink = $this->view->link( $depTitle, $depTitleStr ); |
| 306 | + return self::fatalError( 'qp_error_missed_dependance_title', $this->mPollId, $depLink, $depPollId ); |
| 307 | + } else { |
286 | 308 | return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn ); |
287 | | - case qp_Setup::ERROR_MISSED_TITLE : |
288 | | - $depSplit = self::getPrefixedPollAddress( $dependsOn ); |
289 | | - if ( is_array( $depSplit ) ) { |
290 | | - list( $depTitleStr, $depPollId ) = $depSplit; |
291 | | - $depTitle = Title::newFromURL( $depTitleStr ); |
292 | | - $depTitleStr = $depTitle->getPrefixedText(); |
293 | | - $depLink = $this->view->link( $depTitle, $depTitleStr ); |
294 | | - return self::fatalError( 'qp_error_missed_dependance_title', $this->mPollId, $depLink, $depPollId ); |
295 | | - } else { |
296 | | - return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $dependsOn ); |
297 | | - } |
298 | | - default : |
299 | | - throw new MWException( __METHOD__ . ' invalid dependance poll store found' ); |
300 | 309 | } |
| 310 | + default : |
| 311 | + throw new MWException( __METHOD__ . ' invalid dependance poll store found' ); |
301 | 312 | } |
302 | | - } else { |
303 | | - return true; |
304 | 313 | } |
305 | 314 | } |
306 | 315 | |
Index: trunk/extensions/QPoll/ctrl/qp_interpresult.php |
— | — | @@ -0,0 +1,111 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * An interpretation result of user answer to the quiz |
| 10 | + */ |
| 11 | +class qp_InterpResult { |
| 12 | + # short answer. it is supposed to be sortable and accountable in statistics |
| 13 | + # by default, it is private (displayed only in Special:Pollresults page) |
| 14 | + # blank value means short answer is unavailable |
| 15 | + var $short = ''; |
| 16 | + # long answer. it is supposed to be understandable by amateur users |
| 17 | + # by default, it is public (displayed everywhere) |
| 18 | + # blank value means long answer is unavailable |
| 19 | + var $long = ''; |
| 20 | + # structured answer. scalar value or an associative array. |
| 21 | + # object instances are not allowed. |
| 22 | + # their purpose is: |
| 23 | + # * exported to XLS cells to be analyzed by external tools |
| 24 | + # * import interpretation results of another polls to |
| 25 | + # current interpretation script make cross-poll (multi-poll) |
| 26 | + # interpretations |
| 27 | + var $structured = ''; |
| 28 | + # error message. non-blank value indicates interpretation script error |
| 29 | + # either due to incorrect script code, or a script-generated one |
| 30 | + var $error = ''; |
| 31 | + # whether the user answer should be stored in case of |
| 32 | + # erroneous interpretation results: |
| 33 | + # true: education application, to see pupul's mistake; |
| 34 | + # false: form validation, to prevent visibility of next poll in |
| 35 | + # dependance chain until the form is filled properly; |
| 36 | + var $storeErroneous = true; |
| 37 | + # interpretation result |
| 38 | + # 2d array of errors generated for [question][proposal] |
| 39 | + # 3d array of errors generated for [question][proposal][category] |
| 40 | + # false if no errors |
| 41 | + var $qpcErrors = false; |
| 42 | + |
| 43 | + /** |
| 44 | + * @param $init - optional array of properties to be initialized |
| 45 | + */ |
| 46 | + function __construct( $init = null ) { |
| 47 | + $props = array( 'short', 'long', 'error' ); |
| 48 | + if ( is_array( $init ) ) { |
| 49 | + foreach ( $props as $prop ) { |
| 50 | + if ( array_key_exists( $prop, $init ) ) { |
| 51 | + $this->{ $prop } = $init[$prop]; |
| 52 | + } |
| 53 | + } |
| 54 | + return; |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * "global" error message |
| 60 | + */ |
| 61 | + function setError( $msg ) { |
| 62 | + $this->error = $msg; |
| 63 | + return $this; |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * set question / proposal error message (for quizes) |
| 68 | + * |
| 69 | + * @param $msg string error message for [question][proposal] pair; |
| 70 | + * non-string for default message |
| 71 | + * @param $qidx int index of poll's question |
| 72 | + * @param $pidx int index of question's proposal |
| 73 | + * @param $cidx int index of proposal's category (optional) |
| 74 | + */ |
| 75 | + function setQPCerror( $msg, $qidx, $pidx, $cidx = null ) { |
| 76 | + if ( !is_array( $this->qpcErrors ) ) { |
| 77 | + $this->qpcErrors = array(); |
| 78 | + } |
| 79 | + if ( !array_key_exists( $qidx, $this->qpcErrors ) ) { |
| 80 | + $this->qpcErrors[$qidx] = array(); |
| 81 | + } |
| 82 | + if ( $cidx === null ) { |
| 83 | + # proposal interpretation error message |
| 84 | + $this->qpcErrors[$qidx][$pidx] = $msg; |
| 85 | + return; |
| 86 | + } |
| 87 | + # proposal's category interpretation error message |
| 88 | + if ( !array_key_exists( $pidx, $this->qpcErrors[$qidx] ) || |
| 89 | + !is_array( $this->qpcErrors[$qidx][$pidx] ) ) { |
| 90 | + # remove previous proposal interpretation error message because |
| 91 | + # now we have more precise category interpretation error message |
| 92 | + $this->qpcErrors[$qidx][$pidx] = array(); |
| 93 | + } |
| 94 | + $this->qpcErrors[$qidx][$pidx][$cidx] = $msg; |
| 95 | + } |
| 96 | + |
| 97 | + function setDefaultErrorMessage() { |
| 98 | + if ( is_array( $this->qpcErrors ) && $this->error == '' ) { |
| 99 | + $this->error = wfMsg( 'qp_interpetation_wrong_answer' ); |
| 100 | + } |
| 101 | + return $this; |
| 102 | + } |
| 103 | + |
| 104 | + function isError() { |
| 105 | + return $this->error != '' || is_array( $this->qpcErrors ); |
| 106 | + } |
| 107 | + |
| 108 | + function hasToBeStored() { |
| 109 | + return !$this->isError() || $this->storeErroneous; |
| 110 | + } |
| 111 | + |
| 112 | +} /* end of qp_InterpResult class */ |
Property changes on: trunk/extensions/QPoll/ctrl/qp_interpresult.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 113 | + native |
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php |
— | — | @@ -22,10 +22,11 @@ |
23 | 23 | # some questions has a subtype; currently is not stored in DB; |
24 | 24 | # should always be properly initialized in parent controller via $poll->parseMainHeader() |
25 | 25 | var $mSubType = ''; |
26 | | - var $mCategories = Array(); |
27 | | - var $mCategorySpans = Array(); |
| 26 | + var $mCategories = array(); |
| 27 | + var $mCategorySpans = array(); |
28 | 28 | var $mCommonQuestion = ''; // common question of this question |
29 | | - var $mProposalText = Array(); // an array of question proposals |
| 29 | + var $mProposalNames = array(); // an array of question proposals names (optional, used in interpretation scripts) |
| 30 | + var $mProposalText = array(); // an array of question proposals |
30 | 31 | var $alreadyVoted = false; // whether the selected user has already voted this question ? |
31 | 32 | |
32 | 33 | # statistics |
— | — | @@ -92,6 +93,10 @@ |
93 | 94 | $this->view->setPropWidth( $paramkeys[ 'propwidth' ] ); |
94 | 95 | } |
95 | 96 | |
| 97 | + function getProposalIdByName( $proposalName ) { |
| 98 | + return array_search( $proposalName, $this->mProposalNames, true ); |
| 99 | + } |
| 100 | + |
96 | 101 | function getPercents( $proposalId, $catId ) { |
97 | 102 | if ( is_array( $this->Percents ) && array_key_exists( $proposalId, $this->Percents ) && |
98 | 103 | is_array( $this->Percents[ $proposalId ] ) && array_key_exists( $catId, $this->Percents[ $proposalId ] ) ) { |
Index: trunk/extensions/QPoll/ctrl/question/qp_textquestion.php |
— | — | @@ -175,10 +175,13 @@ |
176 | 176 | $proposalId = 0; |
177 | 177 | # Currently, we use just a single instance (no nested categories) |
178 | 178 | $opt = new qp_TextQuestionOptions(); |
| 179 | + # set static view state for the future qp_TextQuestionProposalView instances |
179 | 180 | qp_TextQuestionProposalView::applyViewState( $this->view ); |
180 | 181 | foreach ( $this->raws as $raw ) { |
181 | 182 | $opt->reset(); |
182 | 183 | $this->propview = new qp_TextQuestionProposalView( $proposalId, $this ); |
| 184 | + # set proposal name (if any) |
| 185 | + $prop_name = qp_QuestionData::splitRawProposal( $raw ); |
183 | 186 | $this->dbtokens = $brace_stack = array(); |
184 | 187 | $catId = 0; |
185 | 188 | $last_brace = ''; |
— | — | @@ -245,13 +248,18 @@ |
246 | 249 | # todo: this is the explanary line, it is not real proposal |
247 | 250 | $this->propview->prependErrorToken( wfMsg( 'qp_error_too_few_categories' ), 'error' ); |
248 | 251 | } |
249 | | - if ( strlen( $proposal_text = serialize( $this->dbtokens ) ) > qp_Setup::$proposal_max_length ) { |
| 252 | + $proposal_text = serialize( $this->dbtokens ); |
| 253 | + # build the whole raw DB proposal_text value to check it's maximal length |
| 254 | + if ( strlen( qp_QuestionData::getProposalNamePrefix( $prop_name ) . $proposal_text ) > qp_Setup::$proposal_max_length ) { |
250 | 255 | # too long proposal field to store into the DB |
251 | 256 | # this is very important check for text questions because |
252 | 257 | # category definitions are stored within the proposal text |
253 | 258 | $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' ); |
254 | 259 | } |
255 | 260 | $this->mProposalText[$proposalId] = $proposal_text; |
| 261 | + if ( $prop_name !== '' ) { |
| 262 | + $this->mProposalNames[$proposalId] = $prop_name; |
| 263 | + } |
256 | 264 | if ( $this->poll->mBeingCorrected ) { |
257 | 265 | # check for unanswered categories |
258 | 266 | try { |
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php |
— | — | @@ -21,6 +21,7 @@ |
22 | 22 | } |
23 | 23 | $this->mProposalPattern .= '(.*)`u'; |
24 | 24 | $proposalId = -1; |
| 25 | + # set static view state for the future qp_TabularQuestionProposalView instances |
25 | 26 | qp_TabularQuestionProposalView::applyViewState( $this->view ); |
26 | 27 | foreach ( $this->raws as $raw ) { |
27 | 28 | # new proposal view |
— | — | @@ -40,7 +41,11 @@ |
41 | 42 | continue; |
42 | 43 | } |
43 | 44 | $proposalId++; |
44 | | - $this->mProposalText[ $proposalId ] = trim( $pview->text ); |
| 45 | + # set proposal name (if any) |
| 46 | + if ( ( $prop_name = qp_QuestionData::splitRawProposal( $pview->text ) ) !== '' ) { |
| 47 | + $this->mProposalNames[$proposalId] = $prop_name; |
| 48 | + } |
| 49 | + $this->mProposalText[$proposalId] = trim( $pview->text ); |
45 | 50 | # Determine a type ID, according to the questionType and the number of signes. |
46 | 51 | foreach ( $this->mCategories as $catId => $catDesc ) { |
47 | 52 | $typeId = $matches[ $catId ]; |
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php |
— | — | @@ -271,6 +271,7 @@ |
272 | 272 | */ |
273 | 273 | function questionParseBody( $inputType ) { |
274 | 274 | $proposalId = -1; |
| 275 | + # set static view state for the future qp_TabularQuestionProposalView instances |
275 | 276 | qp_TabularQuestionProposalView::applyViewState( $this->view ); |
276 | 277 | foreach ( $this->raws as $raw ) { |
277 | 278 | if ( !preg_match( $this->mProposalPattern, $raw, $matches ) ) { |
— | — | @@ -280,7 +281,11 @@ |
281 | 282 | $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this ); |
282 | 283 | $proposalId++; |
283 | 284 | $pview->text = array_pop( $matches ); |
284 | | - $this->mProposalText[ $proposalId ] = trim( $pview->text ); |
| 285 | + # set proposal name (if any) |
| 286 | + if ( ( $prop_name = qp_QuestionData::splitRawProposal( $pview->text ) ) !== '' ) { |
| 287 | + $this->mProposalNames[$proposalId] = $prop_name; |
| 288 | + } |
| 289 | + $this->mProposalText[$proposalId] = trim( $pview->text ); |
285 | 290 | foreach ( $this->mCategories as $catId => $catDesc ) { |
286 | 291 | # start new input field tag (category) |
287 | 292 | $pview->addNewCategory( $catId ); |
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php |
— | — | @@ -98,6 +98,7 @@ |
99 | 99 | 'categories' => $this->mCategories, |
100 | 100 | 'category_spans' => $this->mCategorySpans, |
101 | 101 | 'proposal_text' => $this->mProposalText, |
| 102 | + 'proposal_names' => $this->mProposalNames, |
102 | 103 | 'proposal_category_id' => $this->mProposalCategoryId, |
103 | 104 | 'proposal_category_text' => $this->mProposalCategoryText ) ); |
104 | 105 | } |
Index: trunk/extensions/QPoll/qp_user.php |
— | — | @@ -89,7 +89,7 @@ |
90 | 90 | if ( count( $args ) < 1 ) { |
91 | 91 | return; |
92 | 92 | } |
93 | | - $message = $args[0]; |
| 93 | + $message = strval( $args[0] ); |
94 | 94 | $debug = true; |
95 | 95 | if ( count( $args ) > 2 ) { |
96 | 96 | $debug = $args[2]; |
— | — | @@ -116,6 +116,26 @@ |
117 | 117 | } |
118 | 118 | |
119 | 119 | /** |
| 120 | + * Returns either scalar or associative array with structured interpretation |
| 121 | + * of the specified poll for the current user |
| 122 | + * |
| 123 | + * @return scalar/array when success; null when there is no structured interpretation |
| 124 | + */ |
| 125 | +function qp_getStructuredInterpretation( $poll_address ) { |
| 126 | + $pollStore = qp_PollStore::newFromAddr( $poll_address ); |
| 127 | + if ( !( $pollStore instanceof qp_PollStore ) || $pollStore->pid === null ) { |
| 128 | + return null; |
| 129 | + } |
| 130 | + $username = qp_Setup::getCurrUserName(); |
| 131 | + $pollStore->loadQuestions(); |
| 132 | + $pollStore->setLastUser( $username, false ); |
| 133 | + if ( $pollStore->interpResult->structured === '' ) { |
| 134 | + return null; |
| 135 | + } |
| 136 | + return unserialize( $pollStore->interpResult->structured ); |
| 137 | +} |
| 138 | + |
| 139 | +/** |
120 | 140 | * Extension's global settings and initializiers |
121 | 141 | * should be purely static and preferrably have no constructor |
122 | 142 | */ |
— | — | @@ -206,9 +226,9 @@ |
207 | 227 | public static $structured_interpretation_max_length = 65535; |
208 | 228 | # whether to show short, long, structured interpretation results to end user |
209 | 229 | public static $show_interpretation = array( |
210 | | - 'short' => false, |
| 230 | + 'short' => true, |
211 | 231 | 'long' => true, |
212 | | - 'structured' => false |
| 232 | + 'structured' => true |
213 | 233 | ); |
214 | 234 | /* end of default configuration settings */ |
215 | 235 | |
— | — | @@ -267,13 +287,11 @@ |
268 | 288 | 'qp_user.php' => 'qp_Setup', |
269 | 289 | 'includes/qp_functionshook.php' => 'qp_FunctionsHook', |
270 | 290 | 'includes/qp_renderer.php' => 'qp_Renderer', |
| 291 | + 'includes/qp_excel.php' => 'qp_Excel', |
271 | 292 | |
272 | 293 | ## DB schema updater |
273 | 294 | 'maintenance/qp_schemaupdater.php' => 'qp_SchemaUpdater', |
274 | 295 | |
275 | | - ## collection of the questions |
276 | | - 'qp_question_collection.php' => 'qp_QuestionCollection', |
277 | | - |
278 | 296 | ## controllers (polls and questions derived from separate abstract classes) |
279 | 297 | # polls |
280 | 298 | 'ctrl/poll/qp_abstractpoll.php' => 'qp_AbstractPoll', |
— | — | @@ -286,6 +304,8 @@ |
287 | 305 | 'ctrl/question/qp_mixedquestion.php' => 'qp_MixedQuestion', |
288 | 306 | 'ctrl/question/qp_textquestion.php' => array( 'qp_TextQuestionOptions', 'qp_TextQuestion' ), |
289 | 307 | 'ctrl/question/qp_questionstats.php' => 'qp_QuestionStats', |
| 308 | + # interpretation results |
| 309 | + 'ctrl/qp_interpresult.php' => 'qp_InterpResult', |
290 | 310 | |
291 | 311 | # generic view |
292 | 312 | 'view/qp_abstractview.php' => 'qp_AbstractView', |
— | — | @@ -300,28 +320,34 @@ |
301 | 321 | 'view/question/qp_tabularquestionview.php' => 'qp_TabularQuestionView', |
302 | 322 | 'view/question/qp_textquestionview.php' => 'qp_TextQuestionView', |
303 | 323 | 'view/question/qp_questionstatsview.php' => 'qp_QuestionStatsView', |
304 | | - ## proposal views are derived from it's own separate abstract class |
| 324 | + ## proposal views are derived from their own base abstract class |
305 | 325 | # proposals |
306 | 326 | 'view/proposal/qp_stubquestionproposalview.php' => 'qp_StubQuestionProposalView', |
307 | 327 | 'view/proposal/qp_tabularquestionproposalview.php' => 'qp_TabularQuestionProposalView', |
308 | 328 | 'view/proposal/qp_questionstatsproposalview.php' => 'qp_QuestionStatsProposalView', |
309 | 329 | 'view/proposal/qp_textquestionproposalview.php' => 'qp_TextQuestionProposalView', |
| 330 | + ## question data views are used to display question results in Special:PollResults page |
| 331 | + 'view/results/qp_questiondataresults.php' => 'qp_QuestionDataResults', |
| 332 | + 'view/results/qp_textquestiondataresults.php' => 'qp_TextQuestionDataResults', |
| 333 | + # interpretation results |
| 334 | + 'view/qp_interpresultview.php' => 'qp_InterpResultView', |
310 | 335 | |
311 | | - # poll storage |
312 | | - 'qp_pollstore.php' => array( 'qp_InterpResult', 'qp_PollStore' ), |
| 336 | + ## models/storage |
| 337 | + # poll |
| 338 | + 'model/qp_pollstore.php' => 'qp_PollStore', |
| 339 | + # question storage; qp_TextQuestionData is very small, thus kept in the same file; |
| 340 | + 'model/qp_questiondata.php' => array( 'qp_QuestionData', 'qp_TextQuestionData' ), |
| 341 | + # collection of the questions |
| 342 | + 'model/qp_question_collection.php' => 'qp_QuestionCollection', |
313 | 343 | |
314 | | - # question storage and page result question views |
315 | | - # (combined question storage & view) |
316 | | - 'qp_questiondata.php' => array( 'qp_QuestionData', 'qp_TextQuestionData' ), |
317 | | - |
318 | 344 | # special pages |
319 | 345 | 'specials/qp_special.php' => array( 'qp_SpecialPage', 'qp_QueryPage' ), |
320 | 346 | 'specials/qp_results.php' => 'PollResults', |
321 | 347 | 'specials/qp_webinstall.php' => array( 'qp_WebInstall' ), |
322 | 348 | |
323 | 349 | # interpretation of answers |
324 | | - 'qp_interpret.php' => 'qp_Interpret', |
325 | | - 'qp_eval.php' => 'qp_Eval' |
| 350 | + 'interpretation/qp_interpret.php' => 'qp_Interpret', |
| 351 | + 'interpretation/qp_eval.php' => 'qp_Eval' |
326 | 352 | ) ); |
327 | 353 | |
328 | 354 | # TODO: Use the new technique for i18n of special page aliases |
Index: trunk/extensions/QPoll/includes/qp_excel.php |
— | — | @@ -0,0 +1,72 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of QPoll. |
| 6 | + * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
| 7 | + * |
| 8 | + * QPoll is free software; you can redistribute it and/or modify |
| 9 | + * it under the terms of the GNU General Public License as published by |
| 10 | + * the Free Software Foundation; either version 2 of the License, or |
| 11 | + * (at your option) any later version. |
| 12 | + * |
| 13 | + * QPoll is distributed in the hope that it will be useful, |
| 14 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | + * GNU General Public License for more details. |
| 17 | + * |
| 18 | + * You should have received a copy of the GNU General Public License |
| 19 | + * along with QPoll; if not, write to the Free Software |
| 20 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 21 | + * |
| 22 | + * ***** END LICENSE BLOCK ***** |
| 23 | + * |
| 24 | + * QPoll is a poll tool for MediaWiki. |
| 25 | + * |
| 26 | + * To activate this extension : |
| 27 | + * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
| 28 | + * * Place the files from the extension archive there. |
| 29 | + * * Add this line at the end of your LocalSettings.php file : |
| 30 | + * require_once "$IP/extensions/QPoll/qp_user.php"; |
| 31 | + * |
| 32 | + * @version 0.8.0a |
| 33 | + * @link http://www.mediawiki.org/wiki/Extension:QPoll |
| 34 | + * @author QuestPC <questpc@rambler.ru> |
| 35 | + */ |
| 36 | + |
| 37 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * PEAR Excel helper |
| 43 | + * |
| 44 | + * todo: PollResults::voicesToXLS() and PollResults::statsToXLS() should be refactored into |
| 45 | + * this class; |
| 46 | + * |
| 47 | + */ |
| 48 | +class qp_Excel { |
| 49 | + |
| 50 | + static function prepareExcelString( $s ) { |
| 51 | + if ( preg_match( '`^=.?`', $s ) ) { |
| 52 | + return "'" . $s; |
| 53 | + } |
| 54 | + return $s; |
| 55 | + } |
| 56 | + |
| 57 | + static function writeFormattedTable( $worksheet, $rownum, $colnum, &$table, $format = null ) { |
| 58 | + foreach ( $table as $rnum => &$row ) { |
| 59 | + foreach ( $row as $cnum => &$cell ) { |
| 60 | + if ( is_array( $cell ) ) { |
| 61 | + if ( array_key_exists( "format", $cell ) ) { |
| 62 | + $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell[ 0 ], $cell[ "format" ] ); |
| 63 | + } else { |
| 64 | + $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell[ 0 ], $format ); |
| 65 | + } |
| 66 | + } else { |
| 67 | + $worksheet->write( $rownum + $rnum, $colnum + $cnum, $cell, $format ); |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | +} /* end of qp_Excel class */ |
Property changes on: trunk/extensions/QPoll/includes/qp_excel.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 74 | + native |
Index: trunk/extensions/QPoll/interpretation/qp_interpret.php |
— | — | @@ -0,0 +1,165 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of QPoll. |
| 6 | + * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
| 7 | + * |
| 8 | + * QPoll is free software; you can redistribute it and/or modify |
| 9 | + * it under the terms of the GNU General Public License as published by |
| 10 | + * the Free Software Foundation; either version 2 of the License, or |
| 11 | + * (at your option) any later version. |
| 12 | + * |
| 13 | + * QPoll is distributed in the hope that it will be useful, |
| 14 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | + * GNU General Public License for more details. |
| 17 | + * |
| 18 | + * You should have received a copy of the GNU General Public License |
| 19 | + * along with QPoll; if not, write to the Free Software |
| 20 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 21 | + * |
| 22 | + * ***** END LICENSE BLOCK ***** |
| 23 | + * |
| 24 | + * QPoll is a poll tool for MediaWiki. |
| 25 | + * |
| 26 | + * To activate this extension : |
| 27 | + * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
| 28 | + * * Place the files from the extension archive there. |
| 29 | + * * Add this line at the end of your LocalSettings.php file : |
| 30 | + * require_once "$IP/extensions/QPoll/qp_user.php"; |
| 31 | + * |
| 32 | + * @version 0.8.0a |
| 33 | + * @link http://www.mediawiki.org/wiki/Extension:QPoll |
| 34 | + * @author QuestPC <questpc@rambler.ru> |
| 35 | + */ |
| 36 | + |
| 37 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +class qp_Interpret { |
| 42 | + |
| 43 | + /** |
| 44 | + * Lint the code of specified language by appropriate interpretator |
| 45 | + * @param $lang string language key (eg. 'php') |
| 46 | + * @param $code string source code |
| 47 | + * @return bool true, when code has no syntax errors; |
| 48 | + * string error message from lint |
| 49 | + */ |
| 50 | + static function lint( $lang, $code ) { |
| 51 | + switch ( $lang ) { |
| 52 | + case 'php' : |
| 53 | + return qp_Eval::lint( $code ); |
| 54 | + default : |
| 55 | + # unknown languages syntax is "valid" because it cannot be checked |
| 56 | + return true; |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Glues the content of <qpinterpret> tags together, checks "lang" attribute |
| 62 | + * and calls appropriate interpretator to evaluate the user answer |
| 63 | + * |
| 64 | + * @param $interpArticle _existing_ Article with interpretation script enclosed in <qpinterp> tags |
| 65 | + * @param $injectVars array with the following possible keys: |
| 66 | + * key 'answer' array of user selected categories for |
| 67 | + * every proposal & question of the poll; |
| 68 | + * key 'usedQuestions' array of used questions for randomized polls |
| 69 | + * or false, when the poll questions were not randomized |
| 70 | + * @return instance of qp_InterpResult class (interpretation result) |
| 71 | + */ |
| 72 | + static function getResult( $interpArticle, $injectVars ) { |
| 73 | + global $wgParser; |
| 74 | + $matches = array(); |
| 75 | + # extract <qpinterpret> tags from the article content |
| 76 | + $wgParser->extractTagsAndParams( array( qp_Setup::$interpTag ), $interpArticle->getRawText(), $matches ); |
| 77 | + $interpResult = new qp_InterpResult(); |
| 78 | + # glue content of all <qpinterpret> tags at the page together |
| 79 | + $interpretScript = ''; |
| 80 | + $lang = ''; |
| 81 | + foreach ( $matches as &$match ) { |
| 82 | + list( $tagName, $content, $attrs ) = $match; |
| 83 | + # basic checks for lang attribute (only lang="php" is implemented yet) |
| 84 | + # however we do not want to limit interpretation language, |
| 85 | + # so the attribute is enforced to use |
| 86 | + if ( !isset( $attrs['lang'] ) ) { |
| 87 | + return $interpResult->setError( wfMsg( 'qp_error_eval_missed_lang_attr' ) ); |
| 88 | + } |
| 89 | + if ( $lang == '' ) { |
| 90 | + $lang = $attrs['lang']; |
| 91 | + } elseif ( $attrs['lang'] != $lang ) { |
| 92 | + return $interpResult->setError( wfMsg( 'qp_error_eval_mix_languages', $lang, $attrs['lang'] ) ); |
| 93 | + } |
| 94 | + if ( $tagName == qp_Setup::$interpTag ) { |
| 95 | + $interpretScript .= $content; |
| 96 | + } |
| 97 | + } |
| 98 | + switch ( $lang ) { |
| 99 | + case 'php' : |
| 100 | + $result = qp_Eval::interpretAnswer( $interpretScript, $injectVars, $interpResult ); |
| 101 | + if ( $result instanceof qp_InterpResult ) { |
| 102 | + # evaluation error (environment error) , return it; |
| 103 | + return $interpResult; |
| 104 | + } |
| 105 | + break; |
| 106 | + default : |
| 107 | + return $interpResult->setError( wfMsg( 'qp_error_eval_unsupported_language', $lang ) ); |
| 108 | + } |
| 109 | + /*** process the result ***/ |
| 110 | + if ( !is_array( $result ) ) { |
| 111 | + return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
| 112 | + } |
| 113 | + if ( isset( $result['options'] ) && $result['options'] === 'noerrorstorage' ) { |
| 114 | + $interpResult->storeErroneous = false; |
| 115 | + } |
| 116 | + if ( isset( $result['error'] ) && is_array( $result['error'] ) ) { |
| 117 | + # initialize $interpResult->qpcErrors[] member array |
| 118 | + foreach ( $result['error'] as $qidx => $question ) { |
| 119 | + if ( is_int( $qidx ) && is_array( $question ) ) { |
| 120 | + foreach ( $question as $pidx => $prop_error ) { |
| 121 | + # integer indicates proposal id; string - proposal name |
| 122 | + if ( is_int( $pidx ) || is_string( $pidx ) ) { |
| 123 | + if ( is_array( $prop_error ) ) { |
| 124 | + # separate error messages list for proposal categories |
| 125 | + foreach ( $prop_error as $cidx => $cat_error ) { |
| 126 | + if ( is_int( $cidx ) ) { |
| 127 | + $interpResult->setQPCerror( $cat_error, $qidx, $pidx, $cidx ); |
| 128 | + } |
| 129 | + } |
| 130 | + } else { |
| 131 | + # error message for the whole proposal line |
| 132 | + $interpResult->setQPCerror( $prop_error, $qidx, $pidx ); |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + if ( isset( $result['errmsg'] ) && trim( strval( $result['errmsg'] ) ) != '' ) { |
| 140 | + # script-generated error message for the whole answer |
| 141 | + return $interpResult->setError( (string) $result['errmsg'] ); |
| 142 | + } |
| 143 | + # if there were question/proposal errors, return them; |
| 144 | + if ( $interpResult->isError() ) { |
| 145 | + return $interpResult->setDefaultErrorMessage(); |
| 146 | + } |
| 147 | + $interpCount = 0; |
| 148 | + foreach ( qp_Setup::$show_interpretation as $interpType => $show ) { |
| 149 | + if ( isset( $result[$interpType] ) ) { |
| 150 | + $interpCount++; |
| 151 | + } |
| 152 | + } |
| 153 | + if ( $interpCount == 0 ) { |
| 154 | + return $interpResult->setError( wfMsg( 'qp_error_interpretation_no_return' ) ); |
| 155 | + } |
| 156 | + $interpResult->structured = isset( $result['structured'] ) ? serialize( $result['structured'] ) : ''; |
| 157 | + if ( strlen( $interpResult->structured ) > qp_Setup::$structured_interpretation_max_length ) { |
| 158 | + unset( $interpResult->structured ); |
| 159 | + return $interpResult->setError( wfMsg( 'qp_error_structured_interpretation_is_too_long' ) ); |
| 160 | + } |
| 161 | + $interpResult->short = isset( $result['short'] ) ? strval( $result['short'] ) : ''; |
| 162 | + $interpResult->long = isset( $result['long'] ) ? strval( $result['long'] ) : ''; |
| 163 | + return $interpResult; |
| 164 | + } |
| 165 | + |
| 166 | +} /* end of qp_Interpret class */ |
Property changes on: trunk/extensions/QPoll/interpretation/qp_interpret.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 167 | + native |
Index: trunk/extensions/QPoll/interpretation/qp_eval.php |
— | — | @@ -0,0 +1,417 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * ***** BEGIN LICENSE BLOCK ***** |
| 5 | + * This file is part of QPoll. |
| 6 | + * Uses parts of code from Quiz extension (c) 2007 Louis-Rémi BABE. All rights reserved. |
| 7 | + * |
| 8 | + * QPoll is free software; you can redistribute it and/or modify |
| 9 | + * it under the terms of the GNU General Public License as published by |
| 10 | + * the Free Software Foundation; either version 2 of the License, or |
| 11 | + * (at your option) any later version. |
| 12 | + * |
| 13 | + * QPoll is distributed in the hope that it will be useful, |
| 14 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | + * GNU General Public License for more details. |
| 17 | + * |
| 18 | + * You should have received a copy of the GNU General Public License |
| 19 | + * along with QPoll; if not, write to the Free Software |
| 20 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 21 | + * |
| 22 | + * ***** END LICENSE BLOCK ***** |
| 23 | + * |
| 24 | + * QPoll is a poll tool for MediaWiki. |
| 25 | + * |
| 26 | + * To activate this extension : |
| 27 | + * * Create a new directory named QPoll into the directory "extensions" of MediaWiki. |
| 28 | + * * Place the files from the extension archive there. |
| 29 | + * * Add this line at the end of your LocalSettings.php file : |
| 30 | + * require_once "$IP/extensions/QPoll/qp_user.php"; |
| 31 | + * |
| 32 | + * @version 0.8.0a |
| 33 | + * @link http://www.mediawiki.org/wiki/Extension:QPoll |
| 34 | + * @author QuestPC <questpc@rambler.ru> |
| 35 | + */ |
| 36 | + |
| 37 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 38 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 39 | +} |
| 40 | + |
| 41 | +class qp_Eval { |
| 42 | + |
| 43 | + # the list of allowed PHP tokens |
| 44 | + # filtered using the complete list at http://www.php.net/manual/ru/tokens.php |
| 45 | + # is it bullet-proof enough? |
| 46 | + static $allowedTokens = array( |
| 47 | + T_AND_EQUAL, |
| 48 | + T_ARRAY, |
| 49 | + T_AS, |
| 50 | + T_BOOLEAN_AND, |
| 51 | + T_BOOLEAN_OR, |
| 52 | + T_BOOL_CAST, |
| 53 | + T_BREAK, |
| 54 | + T_CASE, |
| 55 | + T_COMMENT, |
| 56 | + T_CONCAT_EQUAL, |
| 57 | + T_CONSTANT_ENCAPSED_STRING, |
| 58 | + T_CONTINUE, |
| 59 | + T_DEC, |
| 60 | + T_DEFAULT, |
| 61 | + T_DIV_EQUAL, |
| 62 | + T_DNUMBER, |
| 63 | + T_DOC_COMMENT, |
| 64 | + T_DOUBLE_ARROW, |
| 65 | + T_DOUBLE_CAST, |
| 66 | + T_ELSE, |
| 67 | + T_ELSEIF, |
| 68 | + T_EMPTY, |
| 69 | + T_ENCAPSED_AND_WHITESPACE, |
| 70 | + T_ENDFOREACH, |
| 71 | + T_ENDIF, |
| 72 | + T_ENDSWITCH, |
| 73 | + T_END_HEREDOC, |
| 74 | + T_FOREACH, |
| 75 | + T_FUNCTION, |
| 76 | + T_IF, |
| 77 | + T_INC, |
| 78 | + T_INT_CAST, |
| 79 | + T_ISSET, |
| 80 | + T_IS_EQUAL, |
| 81 | + T_IS_GREATER_OR_EQUAL, |
| 82 | + T_IS_IDENTICAL, |
| 83 | + T_IS_NOT_EQUAL, |
| 84 | + T_IS_NOT_IDENTICAL, |
| 85 | + T_IS_SMALLER_OR_EQUAL, |
| 86 | + T_LIST, |
| 87 | + T_LNUMBER, |
| 88 | + T_LOGICAL_AND, |
| 89 | + T_LOGICAL_OR, |
| 90 | + T_LOGICAL_XOR, |
| 91 | + T_MINUS_EQUAL, |
| 92 | + T_MOD_EQUAL, |
| 93 | + T_MUL_EQUAL, |
| 94 | + T_NUM_STRING, |
| 95 | + T_OR_EQUAL, |
| 96 | + T_PLUS_EQUAL, |
| 97 | + T_RETURN, |
| 98 | + T_SL, |
| 99 | + T_SL_EQUAL, |
| 100 | + T_SR, |
| 101 | + T_SR_EQUAL, |
| 102 | + T_START_HEREDOC, |
| 103 | + T_STRING, |
| 104 | + T_STRING_CAST, |
| 105 | + T_SWITCH, |
| 106 | + T_UNSET, |
| 107 | + T_UNSET_CAST, |
| 108 | + T_VARIABLE, |
| 109 | + T_WHITESPACE, |
| 110 | + T_XOR_EQUAL |
| 111 | + ); |
| 112 | + |
| 113 | + # allowed functions |
| 114 | + static $allowedCalls = array( |
| 115 | + # math |
| 116 | + 'round', 'ceil', 'floor', |
| 117 | + # arrays |
| 118 | + 'is_array', 'array_key_exists', 'array_search', 'count', 'array_intersect', 'array_diff', |
| 119 | + 'sort', 'asort', 'rsort', 'arsort', |
| 120 | + # types check and conversion |
| 121 | + 'is_numeric', 'ctype_digit', 'intval', 'strval', 'floatval', |
| 122 | + # strings |
| 123 | + 'trim', 'preg_match', 'preg_match_all', 'preg_split', 'qp_lc', |
| 124 | + # importing of structured interpretation from another polls |
| 125 | + 'qp_getStructuredInterpretation', |
| 126 | + # debug |
| 127 | + 'qp_debug' |
| 128 | + ); |
| 129 | + |
| 130 | + # disallowed superglobals |
| 131 | + static $superGlobals = array( |
| 132 | + '$GLOBALS', |
| 133 | + '$_SERVER', |
| 134 | + '$_GET', |
| 135 | + '$_POST', |
| 136 | + '$_FILES', |
| 137 | + '$_REQUEST', |
| 138 | + '$_SESSION', |
| 139 | + '$_ENV', |
| 140 | + '$_COOKIE', |
| 141 | + '$php_errormsg', |
| 142 | + '$HTTP_RAW_POST_DATA', |
| 143 | + '$http_response_header', |
| 144 | + '$argc', |
| 145 | + '$argv' |
| 146 | + ); |
| 147 | + |
| 148 | + # prefix added to local variable names which prevents |
| 149 | + # from accessing local scope variables in eval'ed code |
| 150 | + static $pseudoNamespace = 'qpv_'; |
| 151 | + |
| 152 | + # the list of disallowed code |
| 153 | + # please add new entries, if needed. |
| 154 | + # key 'badresult' means that formally the code is allowed, |
| 155 | + # however the returned result has to be checked |
| 156 | + # (eg. variable substitution is incorrect) |
| 157 | + static $disallowedCode = array( |
| 158 | + array( |
| 159 | + 'code' => '$test = $_SERVER["REQUEST_URI"];', |
| 160 | + 'desc' => 'Disallow reading from superglobals' |
| 161 | + ), |
| 162 | + array( |
| 163 | + 'code' => '$GLOBALS["wgVersion"] = "test";', |
| 164 | + 'desc' => 'Disallow writing to superglobals' |
| 165 | + ), |
| 166 | + array( |
| 167 | + 'code' => 'global $wgVersion;', |
| 168 | + 'desc' => 'Disallow visibility of globals in local scope' |
| 169 | + ), |
| 170 | + array( |
| 171 | + 'code' => 'return $selfCheck == 1;', |
| 172 | + 'badresult' => true, |
| 173 | + 'desc' => 'Disallow access to extension\'s locals in the eval scope' |
| 174 | + ), |
| 175 | + array( |
| 176 | + 'code' => '$writevar = 1; $var = "writevar"; $$var = "test";', |
| 177 | + 'desc' => 'Disallow writing to variable variables' |
| 178 | + ), |
| 179 | + array( |
| 180 | + 'code' => '$readvar = 1; $var = "readvar"; $test = $$var;', |
| 181 | + 'desc' => 'Disallow reading from variable variables' |
| 182 | + ), |
| 183 | + array( |
| 184 | + 'code' => '$readvar = 1; $var = "readvar"; $test = "my$$var 1";', |
| 185 | + 'desc' => 'Disallow reading from complex variable variables' |
| 186 | + ), |
| 187 | + array( |
| 188 | + 'code' => '$dh = opendir( "./" );', |
| 189 | + 'desc' => 'Disallow illegal function calls' |
| 190 | + ), |
| 191 | + array( |
| 192 | + 'code' => '$func = "opendir"; $dh=$func( "./" );', |
| 193 | + 'desc' => 'Disallow variable function calls' |
| 194 | + ), |
| 195 | + array( |
| 196 | + 'code' => 'return "test$selfCheck result";', |
| 197 | + 'badresult' => 'test1 result', |
| 198 | + 'desc' => 'Disallow extension\'s local scope variables in "simple" complex variables' |
| 199 | + ), |
| 200 | + array( |
| 201 | + 'code' => '$curlydollar = "1"; $var = "test{$curlydollar}a";', |
| 202 | + 'desc' => 'Disallow complex variables (curlydollar)' |
| 203 | + ), |
| 204 | + array( |
| 205 | + 'code' => '$dollarcurly = "1"; $var = "test${dollarcurly}a";', |
| 206 | + 'desc' => 'Disallow complex variables (dollarcurly)' |
| 207 | + ), |
| 208 | + array( |
| 209 | + 'code' => '$obj = new stdClass; $obj = new stdClass(); $obj -> a = 1;', |
| 210 | + 'desc' => 'Disallow creation of objects' |
| 211 | + ), |
| 212 | + array( |
| 213 | + 'code' => '$obj -> a = 1;', |
| 214 | + 'desc' => 'Disallow indirect creation of objects' |
| 215 | + ), |
| 216 | + array( |
| 217 | + 'code' => '$obj = (object) array("a"=>1);', |
| 218 | + 'desc' => 'Disallow cast to objects' |
| 219 | + ), |
| 220 | + array( |
| 221 | + 'code' => 'for ( $i = 0; $i < 1; $i++ ) {};', |
| 222 | + 'desc' => 'Disallow for loops, which easily can be made infinite' |
| 223 | + ) |
| 224 | + ); |
| 225 | + |
| 226 | + /** |
| 227 | + * Calls php interpreter to lint interpretation script code |
| 228 | + * @param $code string with php code |
| 229 | + * @return bool true, when code has no syntax errors; |
| 230 | + * string error message from php lint |
| 231 | + */ |
| 232 | + static function lint( $code ) { |
| 233 | + $pipes = array(); |
| 234 | + $spec = array( |
| 235 | + 0 => array( 'pipe', 'r' ), |
| 236 | + 1 => array( 'pipe', 'w' ), |
| 237 | + 2 => array( 'pipe', 'w' ) |
| 238 | + ); |
| 239 | + if ( !function_exists( 'proc_open' ) ) { |
| 240 | + return wfMsg( 'qp_error_eval_unable_to_lint' ); |
| 241 | + } |
| 242 | + $process = proc_open( 'php -l', $spec, $pipes ); |
| 243 | + if ( !is_resource( $process ) ) { |
| 244 | + return wfMsg( 'qp_error_eval_unable_to_lint' ); |
| 245 | + } |
| 246 | + fwrite( $pipes[0], "<?php $code" ); |
| 247 | + fclose( $pipes[0] ); |
| 248 | + $out = array( 1 => '', 2 => '' ); |
| 249 | + foreach ( $out as $key => &$text ) { |
| 250 | + while ( !feof( $pipes[$key] ) ) { |
| 251 | + $text .= fgets( $pipes[$key], 1024 ); |
| 252 | + } |
| 253 | + fclose( $pipes[$key] ); |
| 254 | + } |
| 255 | + $retval = proc_close( $process ); |
| 256 | + if ( $retval == 0 ) { |
| 257 | + # no lint errors |
| 258 | + return true; |
| 259 | + } |
| 260 | + if ( ( $result = trim( implode( $out ) ) ) == '' ) { |
| 261 | + # lint errors but no meaningful error message |
| 262 | + return wfMsg( 'qp_error_eval_unable_to_lint' ); |
| 263 | + } |
| 264 | + # lint error message |
| 265 | + return $result; |
| 266 | + } |
| 267 | + |
| 268 | + /** |
| 269 | + * Check against the list of known disallowed code (for eval) |
| 270 | + * should be executed before every eval, because PHP upgrade can introduce |
| 271 | + * incompatibility leading to secure hole at any time |
| 272 | + * @return |
| 273 | + */ |
| 274 | + static function selfCheck() { |
| 275 | + # remove unavailable functions from allowed calls list |
| 276 | + foreach ( self::$allowedCalls as $key => $fname ) { |
| 277 | + if ( !function_exists( $fname ) ) { |
| 278 | + unset( self::$allowedCalls[$key] ); |
| 279 | + } |
| 280 | + } |
| 281 | + # the following var is used to check access to extension's locals |
| 282 | + # in the eval scope |
| 283 | + $selfCheck = 1; |
| 284 | + foreach ( self::$disallowedCode as $key => &$sourceCode ) { |
| 285 | + # check source code sample |
| 286 | + $destinationCode = ''; |
| 287 | + $result = self::checkAndTransformCode( $sourceCode['code'], $destinationCode ); |
| 288 | + if ( isset( $sourceCode['badresult'] ) ) { |
| 289 | + # the code is meant to be vaild, however the result may be insecure |
| 290 | + if ( $result !== true ) { |
| 291 | + # there is an error in sample |
| 292 | + return 'Sample error:' . $sourceCode['desc']; |
| 293 | + } |
| 294 | + # suppres PHP notices because some tests are supposed to generate them |
| 295 | + $old_reporting = error_reporting( E_ALL & ~E_NOTICE ); |
| 296 | + $test_result = eval( $destinationCode ); |
| 297 | + error_reporting( $old_reporting ); |
| 298 | + # compare eval() result with "insecure" bad result |
| 299 | + if ( $test_result === $sourceCode['badresult'] ) { |
| 300 | + return $sourceCode['desc']; |
| 301 | + } |
| 302 | + } else { |
| 303 | + # the code meant to be invalid |
| 304 | + if ( $result === true ) { |
| 305 | + # illegal destination code which was passed as vaild |
| 306 | + return $sourceCode['desc']; |
| 307 | + } |
| 308 | + } |
| 309 | + } |
| 310 | + return true; |
| 311 | + } |
| 312 | + |
| 313 | + /** |
| 314 | + * Checks the submitted eval code for errors |
| 315 | + * In case of success returns transformed code, which is safer for eval |
| 316 | + * @param $sourceCode submitted code which has to be eval'ed (no php tags) |
| 317 | + * @param $destinationCode transformed code (in case of success) (no php tags) |
| 318 | + * @return boolean true in case of success, string with error message on failure |
| 319 | + */ |
| 320 | + static function checkAndTransformCode( $sourceCode, &$destinationCode ) { |
| 321 | + |
| 322 | + # tokenizer requires php tags to parse propely, |
| 323 | + # eval(), however requires not to have php tags - weird.. |
| 324 | + $tokens = token_get_all( "<?php $sourceCode ?>" ); |
| 325 | + /* remove <?php ?> */ |
| 326 | + array_shift( $tokens ); |
| 327 | + array_pop( $tokens ); |
| 328 | + |
| 329 | + $destinationCode = ''; |
| 330 | + $prev_token = null; |
| 331 | + foreach ( $tokens as $token ) { |
| 332 | + if ( is_array( $token ) ) { |
| 333 | + list( $token_id, $content, $line ) = $token; |
| 334 | + # check against generic list of disallowed tokens |
| 335 | + if ( !in_array( $token_id, self::$allowedTokens, true ) ) { |
| 336 | + return wfMsg( 'qp_error_eval_illegal_token', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 337 | + } |
| 338 | + if ( $token_id == T_VARIABLE ) { |
| 339 | + $prev_content = is_array( $prev_token ) ? $prev_token[1] : $prev_token; |
| 340 | + preg_match( '`(\$)$`', $prev_content, $matches ); |
| 341 | + # disallow variable variables |
| 342 | + if ( count( $matches ) > 1 && $matches[1] == '$' ) { |
| 343 | + return wfMsg( 'qp_error_eval_variable_variable_access', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 344 | + } |
| 345 | + # disallow superglobals |
| 346 | + if ( in_array( $content, self::$superGlobals ) ) { |
| 347 | + return wfMsg( 'qp_error_eval_illegal_superglobal', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 348 | + } |
| 349 | + # restrict variable names |
| 350 | + preg_match( '`^(\$)([A-Za-z0-9_]*)$`', $content, $matches ); |
| 351 | + if ( count( $matches ) != 3 ) { |
| 352 | + return wfMsg( 'qp_error_eval_illegal_variable_name', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 353 | + } |
| 354 | + # correct variable names into pseudonamespace 'qpv_' |
| 355 | + $content = "\$" . self::$pseudoNamespace . $matches[2]; |
| 356 | + } |
| 357 | + # do not count whitespace as previous token |
| 358 | + if ( $token_id != T_WHITESPACE ) { |
| 359 | + $prev_token = $token; |
| 360 | + } |
| 361 | + # concat corrected token to the destination |
| 362 | + $destinationCode .= $content; |
| 363 | + } else { |
| 364 | + if ( $token == '(' && is_array( $prev_token ) ) { |
| 365 | + list( $token_id, $content, $line ) = $prev_token; |
| 366 | + # disallow variable function calls |
| 367 | + if ( $token_id === T_VARIABLE ) { |
| 368 | + return wfMsg( 'qp_error_eval_variable_function_call', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 369 | + } |
| 370 | + # disallow non-allowed function calls based on the list |
| 371 | + if ( $token_id === T_STRING && array_search( $content, self::$allowedCalls, true ) === false ) { |
| 372 | + return wfMsg( 'qp_error_eval_illegal_function_call', token_name( $token_id ), qp_Setup::specialchars( $content ), $line ); |
| 373 | + } |
| 374 | + } |
| 375 | + $prev_token = $token; |
| 376 | + # concat current token to the destination |
| 377 | + $destinationCode .= $token; |
| 378 | + } |
| 379 | + } |
| 380 | + |
| 381 | + return true; |
| 382 | + } |
| 383 | + |
| 384 | + /** |
| 385 | + * Interpretates the answer with selected script |
| 386 | + * @param $interpretScript string source code of interpretation script |
| 387 | + * @param $injectVars array of PHP data to inject into interpretation script; |
| 388 | + * key of element will become variable name |
| 389 | + * in the interpretation script; |
| 390 | + * value of element will become variable value |
| 391 | + * in the interpretation script; |
| 392 | + * @param $interpResult instance of qp_InterpResult class |
| 393 | + * @modifies $interpResult |
| 394 | + * @return array script result to check, or |
| 395 | + * qp_InterpResult $interpResult (in case of error) |
| 396 | + */ |
| 397 | + static function interpretAnswer( $interpretScript, $injectVars, qp_InterpResult $interpResult ) { |
| 398 | + # template page evaluation |
| 399 | + if ( ( $check = self::selfCheck() ) !== true ) { |
| 400 | + # self-check error |
| 401 | + return $interpResult->setError( wfMsg( 'qp_error_eval_self_check', $check ) ); |
| 402 | + } |
| 403 | + $evalScript = ''; |
| 404 | + if ( ( $check = self::checkAndTransformCode( $interpretScript, $evalScript ) ) !== true ) { |
| 405 | + # possible malicious code |
| 406 | + return $interpResult->setError( $check ); |
| 407 | + } |
| 408 | + # inject poll answer into the interpretation script |
| 409 | + $evalInject = ''; |
| 410 | + foreach ( $injectVars as $varname => $var ) { |
| 411 | + $evalInject .= "\$" . self::$pseudoNamespace . "{$varname} = unserialize( base64_decode( '" . base64_encode( serialize( $var ) ) . "' ) ); "; |
| 412 | + } |
| 413 | + $evalScript = "{$evalInject}/* */ {$evalScript}"; |
| 414 | + $result = eval( $evalScript ); |
| 415 | + return $result; |
| 416 | + } |
| 417 | + |
| 418 | +} |
Property changes on: trunk/extensions/QPoll/interpretation/qp_eval.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 419 | + native |
Index: trunk/extensions/QPoll/view/proposal/qp_stubquestionproposalview.php |
— | — | @@ -8,6 +8,7 @@ |
9 | 9 | |
10 | 10 | # proposal's id |
11 | 11 | var $proposalId; |
| 12 | + |
12 | 13 | # an instance of question's controller |
13 | 14 | var $ctrl; |
14 | 15 | var $rowClass = 'proposal'; |
Index: trunk/extensions/QPoll/view/qp_interpresultview.php |
— | — | @@ -0,0 +1,50 @@ |
| 2 | +<?php |
| 3 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 4 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 5 | +} |
| 6 | + |
| 7 | +/** |
| 8 | + * View interpretation results of polls |
| 9 | + */ |
| 10 | +class qp_InterpResultView extends qp_AbstractView { |
| 11 | + |
| 12 | + static function newFromBaseView( $baseView ) { |
| 13 | + return new self( $baseView->parser, $baseView->ppframe ); |
| 14 | + } |
| 15 | + |
| 16 | + function isCompatibleController( $ctrl ) { |
| 17 | + return $ctrl instanceof qp_InterpResult; |
| 18 | + } |
| 19 | + |
| 20 | + /** |
| 21 | + * Add interpretation results to tagarray of poll view |
| 22 | + */ |
| 23 | + function showInterpResults( &$tagarray ) { |
| 24 | + $ctrl = $this->ctrl; |
| 25 | + if ( ( $scriptError = $ctrl->error ) != '' ) { |
| 26 | + $tagarray[] = array( '__tag' => 'div', 'class' => 'interp_error', qp_Setup::specialchars( $scriptError ) ); |
| 27 | + } |
| 28 | + # output long result, when permitted and available |
| 29 | + if ( qp_Setup::$show_interpretation['long'] && |
| 30 | + ( $answer = $ctrl->long ) !== '' ) { |
| 31 | + $tagarray[] = array( '__tag' => 'div', 'class' => 'interp_answer', qp_Setup::specialchars( $answer ) ); |
| 32 | + } |
| 33 | + # output short result, when permitted and available |
| 34 | + if ( qp_Setup::$show_interpretation['short'] && |
| 35 | + ( $answer = $ctrl->short ) !== '' ) { |
| 36 | + $tagarray[] = array( '__tag' => 'div', 'class' => 'interp_answer', qp_Setup::specialchars( $answer ) ); |
| 37 | + } |
| 38 | + if ( qp_Setup::$show_interpretation['structured'] && |
| 39 | + ( $answer = $ctrl->structured ) !== '' ) { |
| 40 | + $tagarray[] = array( '__tag' => 'div', 'class' => 'interp_answer', $this->renderStructuredAnswer() ); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * todo: how can this be related to structured answer in XLS data export? |
| 46 | + */ |
| 47 | + function renderStructuredAnswer() { |
| 48 | + return 'todo: implement'; |
| 49 | + } |
| 50 | + |
| 51 | +} /* end of qp_InterpResultView class */ |
Property changes on: trunk/extensions/QPoll/view/qp_interpresultview.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 52 | + native |
Index: trunk/extensions/QPoll/view/results/qp_questiondataresults.php |
— | — | @@ -0,0 +1,122 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * Render question data in Special:Pollresults |
| 10 | + * |
| 11 | + * *** Usually instantiated via $qdata->createView() *** |
| 12 | + * |
| 13 | + */ |
| 14 | +class qp_QuestionDataResults { |
| 15 | + |
| 16 | + var $ctrl; |
| 17 | + |
| 18 | + function __construct( qp_QuestionData $ctrl ) { |
| 19 | + $this->ctrl = $ctrl; |
| 20 | + } |
| 21 | + |
| 22 | + protected function categoryentities( $cat ) { |
| 23 | + $cat['name'] = qp_Setup::entities( $cat['name'] ); |
| 24 | + return $cat; |
| 25 | + } |
| 26 | + |
| 27 | + /** |
| 28 | + * @return string html representation of user vote for Special:Pollresults output |
| 29 | + */ |
| 30 | + function displayUserQuestionVote() { |
| 31 | + $ctrl = $this->ctrl; |
| 32 | + $output = "<div class=\"qpoll\">\n" . "<table class=\"qdata\">\n"; |
| 33 | + $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $ctrl->CategorySpans ), array( 'class' => 'spans' ), 'th', array( 'count' => 'colspan', 'name' => 0 ) ); |
| 34 | + $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $ctrl->Categories ), '', 'th', array( 'name' => 0 ) ); |
| 35 | + # multiple choice polls doesn't use real spans, instead, every column is like "span" |
| 36 | + $spansUsed = count( $ctrl->CategorySpans ) > 0 || $ctrl->type == "multipleChoice"; |
| 37 | + foreach ( $ctrl->ProposalText as $propkey => &$proposal_text ) { |
| 38 | + $row = array(); |
| 39 | + foreach ( $ctrl->Categories as $catkey => &$cat_name ) { |
| 40 | + $cell = array( 0 => "" ); |
| 41 | + if ( array_key_exists( $propkey, $ctrl->ProposalCategoryId ) && |
| 42 | + ( $id_key = array_search( $catkey, $ctrl->ProposalCategoryId[ $propkey ] ) ) !== false ) { |
| 43 | + $text_answer = $ctrl->ProposalCategoryText[ $propkey ][ $id_key ]; |
| 44 | + if ( $text_answer != '' ) { |
| 45 | + if ( strlen( $text_answer ) > 20 ) { |
| 46 | + $cell[ 0 ] = array( '__tag' => 'div', 'style' => 'width:10em; height:5em; overflow:auto', 0 => qp_Setup::entities( $text_answer ) ); |
| 47 | + } else { |
| 48 | + $cell[ 0 ] = qp_Setup::entities( $text_answer ); |
| 49 | + } |
| 50 | + } else { |
| 51 | + $cell[ 0 ] = '+'; |
| 52 | + } |
| 53 | + } |
| 54 | + if ( $spansUsed ) { |
| 55 | + if ( $ctrl->type == "multipleChoice" ) { |
| 56 | + $cell[ "class" ] = ( ( $catkey & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
| 57 | + } else { |
| 58 | + $cell[ "class" ] = ( ( $ctrl->Categories[ $catkey ][ "spanId" ] & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
| 59 | + } |
| 60 | + } else { |
| 61 | + $cell[ "class" ] = "stats"; |
| 62 | + } |
| 63 | + $row[] = $cell; |
| 64 | + } |
| 65 | + $row[] = array( 0 => qp_Setup::entities( $proposal_text ), "style" => "text-align:left;" ); |
| 66 | + $output .= qp_Renderer::displayRow( $row ); |
| 67 | + } |
| 68 | + $output .= "</table>\n" . "</div>\n"; |
| 69 | + return $output; |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * @return string html representation of question statistics for Special:Pollresults output |
| 74 | + */ |
| 75 | + function displayQuestionStats( qp_SpecialPage $page, $pid ) { |
| 76 | + $ctrl = $this->ctrl; |
| 77 | + $current_title = $page->getTitle(); |
| 78 | + $output = "<br />\n<b>" . $ctrl->question_id . ".</b> " . qp_Setup::entities( $ctrl->CommonQuestion ) . "<br />\n"; |
| 79 | + $output .= "<div class=\"qpoll\">\n" . "<table class=\"qdata\">\n"; |
| 80 | + $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $ctrl->CategorySpans ), array( 'class' => 'spans' ), 'th', array( 'count' => 'colspan', 'name' => 0 ) ); |
| 81 | + $output .= qp_Renderer::displayRow( array_map( array( $this, 'categoryentities' ), $ctrl->Categories ), '', 'th', array( 'name' => 0 ) ); |
| 82 | + # multiple choice polls doesn't use real spans, instead, every column is like "span" |
| 83 | + $spansUsed = count( $ctrl->CategorySpans ) > 0 || $ctrl->type == "multipleChoice"; |
| 84 | + foreach ( $ctrl->ProposalText as $propkey => &$proposal_text ) { |
| 85 | + if ( isset( $ctrl->Votes[ $propkey ] ) ) { |
| 86 | + if ( $ctrl->Percents === null ) { |
| 87 | + $row = $ctrl->Votes[ $propkey ]; |
| 88 | + } else { |
| 89 | + $row = $ctrl->Percents[ $propkey ]; |
| 90 | + foreach ( $row as $catkey => &$cell ) { |
| 91 | + # Replace spaces with en spaces |
| 92 | + $formatted_cell = str_replace( " ", " ", sprintf( '%3d%%', intval( round( 100 * $cell ) ) ) ); |
| 93 | + # only percents !=0 are displayed as link |
| 94 | + if ( $cell == 0.0 && $ctrl->question_id !== null ) { |
| 95 | + $cell = array( 0 => $formatted_cell, "style" => "color:gray" ); |
| 96 | + } else { |
| 97 | + $cell = array( 0 => $page->qpLink( $current_title, $formatted_cell, |
| 98 | + array( "title" => wfMsgExt( 'qp_votes_count', array( 'parsemag' ), $ctrl->Votes[ $propkey ][ $catkey ] ) ), |
| 99 | + array( "action" => "qpcusers", "id" => $pid, "qid" => $ctrl->question_id, "pid" => $propkey, "cid" => $catkey ) ) ); |
| 100 | + } |
| 101 | + if ( $spansUsed ) { |
| 102 | + if ( $ctrl->type == "multipleChoice" ) { |
| 103 | + $cell[ "class" ] = ( ( $catkey & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
| 104 | + } else { |
| 105 | + $cell[ "class" ] = ( ( $ctrl->Categories[ $catkey ][ "spanId" ] & 1 ) === 0 ) ? "spaneven" : "spanodd"; |
| 106 | + } |
| 107 | + } else { |
| 108 | + $cell[ "class" ] = "stats"; |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + } else { |
| 113 | + # this proposal has no statistics (no votes) |
| 114 | + $row = array_fill( 0, count( $ctrl->Categories ), '' ); |
| 115 | + } |
| 116 | + $row[] = array( 0 => qp_Setup::entities( $proposal_text ), "style" => "text-align:left;" ); |
| 117 | + $output .= qp_Renderer::displayRow( $row ); |
| 118 | + } |
| 119 | + $output .= "</table>\n" . "</div>\n"; |
| 120 | + return $output; |
| 121 | + } |
| 122 | + |
| 123 | +} /* end of qp_QuestionDataResults class */ |
Property changes on: trunk/extensions/QPoll/view/results/qp_questiondataresults.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 124 | + native |
Index: trunk/extensions/QPoll/view/results/qp_textquestiondataresults.php |
— | — | @@ -0,0 +1,69 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + die( "This file is part of the QPoll extension. It is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | + |
| 8 | +/** |
| 9 | + * Render question data in Special:Pollresults |
| 10 | + * |
| 11 | + * *** Usually instantiated via $qdata->createView() *** |
| 12 | + * |
| 13 | + */ |
| 14 | +class qp_TextQuestionDataResults extends qp_QuestionDataResults { |
| 15 | + |
| 16 | + /** |
| 17 | + * @return string html representation of user vote for Special:Pollresults output |
| 18 | + */ |
| 19 | + function displayUserQuestionVote() { |
| 20 | + $ctrl = $this->ctrl; |
| 21 | + $output = "<div class=\"qpoll\">\n" . "<table class=\"qdata\">\n"; |
| 22 | + foreach ( $ctrl->ProposalText as $propkey => &$serialized_tokens ) { |
| 23 | + if ( !is_array( $dbtokens = unserialize( $serialized_tokens ) ) ) { |
| 24 | + throw new MWException( 'dbtokens is not an array in ' . __METHOD__ ); |
| 25 | + } |
| 26 | + $catId = 0; |
| 27 | + $row = array(); |
| 28 | + foreach ( $dbtokens as &$token ) { |
| 29 | + if ( is_string( $token ) ) { |
| 30 | + # add a proposal part |
| 31 | + $row[] = array( '__tag' => 'span', 'class' => 'prop_part', qp_Setup::entities( $token ) ); |
| 32 | + } elseif ( is_array( $token ) ) { |
| 33 | + # add a category definition with selected text answer (if any) |
| 34 | + if ( array_key_exists( $propkey, $ctrl->ProposalCategoryId ) && |
| 35 | + ( $id_key = array_search( $catId, $ctrl->ProposalCategoryId[$propkey] ) ) !== false ) { |
| 36 | + $text_answer = $ctrl->ProposalCategoryText[$propkey][$id_key]; |
| 37 | + } else { |
| 38 | + $text_answer = ''; |
| 39 | + } |
| 40 | + $className = ( count( $token ) === 1 || in_array( $text_answer, $token ) ) ? 'cat_part' : 'cat_unknown'; |
| 41 | + $titleAttr = ''; |
| 42 | + foreach ( $token as &$option ) { |
| 43 | + if ( $option !== $text_answer ) { |
| 44 | + if ( $titleAttr !== '' ) { |
| 45 | + $titleAttr .= ' | '; |
| 46 | + } |
| 47 | + $titleAttr .= qp_Setup::entities( $option ); |
| 48 | + } |
| 49 | + } |
| 50 | + $row[] = array( '__tag' => 'span', 'class' => $className, 'title'=>$titleAttr, qp_Setup::entities( $text_answer ) ); |
| 51 | + # move to the next category (if any) |
| 52 | + $catId++; |
| 53 | + } else { |
| 54 | + throw new MWException( 'DB token has invalid type (' . gettype( $token ) . ') in ' . __METHOD__ ); |
| 55 | + } |
| 56 | + } |
| 57 | + $output .= qp_Renderer::displayRow( array( $row ), array( 'class' => 'qdatatext' ) ); |
| 58 | + } |
| 59 | + $output .= "</table>\n" . "</div>\n"; |
| 60 | + return $output; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * @return string html representation of question statistics for Special:Pollresults output |
| 65 | + */ |
| 66 | + function displayQuestionStats( qp_SpecialPage $page, $pid ) { |
| 67 | + return 'todo: implement'; |
| 68 | + } |
| 69 | + |
| 70 | +} /* end of qp_TextQuestionDataResults class */ |
Property changes on: trunk/extensions/QPoll/view/results/qp_textquestiondataresults.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 71 | + native |
Index: trunk/extensions/QPoll/view/poll/qp_pollview.php |
— | — | @@ -92,17 +92,17 @@ |
93 | 93 | */ |
94 | 94 | function renderPoll() { |
95 | 95 | global $wgOut, $wgRequest; |
| 96 | + $pollStore = $this->ctrl->pollStore; |
96 | 97 | # Generates the output. |
97 | 98 | $qpoll_div = array( '__tag' => 'div', 'class' => 'qpoll' ); |
98 | 99 | $qpoll_div[] = array( '__tag' => 'a', 'name' => $this->ctrl->getPollTitleFragment( null, '' ), 0 => '' ); |
99 | 100 | # output script-generated error, when available |
100 | | - if ( ( $scriptError = $this->ctrl->pollStore->interpResult->error ) != '' ) { |
101 | | - $qpoll_div[] = array( '__tag' => 'div', 'class' => 'interp_error', qp_Setup::specialchars( $scriptError ) ); |
102 | | - } |
103 | | - # output long result, when available |
104 | | - if ( ( $longAnswer = $this->ctrl->pollStore->interpResult->long ) != '' ) { |
105 | | - $qpoll_div[] = array( '__tag' => 'div', 'class' => 'interp_answer', qp_Setup::specialchars( $longAnswer ) ); |
106 | | - } |
| 101 | + # render short/long/structured result, when permitted and available |
| 102 | + $interpResultView = qp_InterpResultView::newFromBaseView( $this ); |
| 103 | + $interpResultView->setController( $pollStore->interpResult ); |
| 104 | + $interpResultView->showInterpResults( $qpoll_div ); |
| 105 | + # unused anymore |
| 106 | + unset( $interpResultView ); |
107 | 107 | # create voting form and fill it with messages and inputs |
108 | 108 | $qpoll_form = array( '__tag' => 'form', 'method' => 'post', 'action' => $this->ctrl->getPollTitleFragment(), 'autocomplete' => 'off', '__end' => "\n" ); |
109 | 109 | $qpoll_div[] = &$qpoll_form; |
— | — | @@ -124,15 +124,15 @@ |
125 | 125 | $qpoll_form[] = array( '__tag' => 'div', 'class' => 'pollQuestions', 0 => $this->renderQuestionViews() ); |
126 | 126 | $submitBtn = array( '__tag' => 'input', 'type' => 'submit' ); |
127 | 127 | $submitMsg = 'qp_vote_button'; |
128 | | - if ( $this->ctrl->pollStore->isAlreadyVoted() ) { |
| 128 | + if ( $pollStore->isAlreadyVoted() ) { |
129 | 129 | $submitMsg = 'qp_vote_again_button'; |
130 | 130 | } |
131 | 131 | if ( $this->ctrl->mBeingCorrected ) { |
132 | | - if ( $this->ctrl->pollStore->getState() == "complete" ) { |
| 132 | + if ( $pollStore->getState() == "complete" ) { |
133 | 133 | $submitMsg = 'qp_vote_again_button'; |
134 | 134 | } |
135 | 135 | } else { |
136 | | - if ( $this->ctrl->pollStore->getState() == "error" ) { |
| 136 | + if ( $pollStore->getState() == "error" ) { |
137 | 137 | $submitBtn[ 'disabled' ] = 'disabled'; |
138 | 138 | } |
139 | 139 | } |
Index: trunk/extensions/QPoll/view/question/qp_tabularquestionview.php |
— | — | @@ -226,7 +226,16 @@ |
227 | 227 | # there is no interpretation error |
228 | 228 | return; |
229 | 229 | } |
230 | | - foreach ( $interpErrors as $prop_id => $prop_desc ) { |
| 230 | + foreach ( $interpErrors as $prop_key => $prop_desc ) { |
| 231 | + if ( is_string( $prop_key ) ) { |
| 232 | + if ( ( $prop_id = $this->ctrl->getProposalIdByName( $prop_key ) ) === false ) { |
| 233 | + continue; |
| 234 | + } |
| 235 | + } elseif ( is_int( $prop_key ) ) { |
| 236 | + $prop_id = $prop_key; |
| 237 | + } else { |
| 238 | + continue; |
| 239 | + } |
231 | 240 | if ( isset( $this->pviews[$prop_id] ) ) { |
232 | 241 | # the whole proposal line has errors |
233 | 242 | $propview = &$this->pviews[$prop_id]; |
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php |
— | — | @@ -86,7 +86,16 @@ |
87 | 87 | # there is no interpretation error |
88 | 88 | return; |
89 | 89 | } |
90 | | - foreach ( $interpErrors as $prop_id => $prop_desc ) { |
| 90 | + foreach ( $interpErrors as $prop_key => $prop_desc ) { |
| 91 | + if ( is_string( $prop_key ) ) { |
| 92 | + if ( ( $prop_id = $this->ctrl->getProposalIdByName( $prop_key ) ) === false ) { |
| 93 | + continue; |
| 94 | + } |
| 95 | + } elseif ( is_int( $prop_key ) ) { |
| 96 | + $prop_id = $prop_key; |
| 97 | + } else { |
| 98 | + continue; |
| 99 | + } |
91 | 100 | if ( isset( $this->pviews[$prop_id] ) ) { |
92 | 101 | # the whole proposal line has errors |
93 | 102 | $propview = &$this->pviews[$prop_id]; |
— | — | @@ -113,7 +122,9 @@ |
114 | 123 | } |
115 | 124 | |
116 | 125 | /** |
117 | | - * |
| 126 | + * Generates tagarray representation from the list of viewtokens |
| 127 | + * @param $viewtokens array of viewtokens |
| 128 | + * @return tagarray |
118 | 129 | */ |
119 | 130 | function renderParsedProposal( &$viewtokens ) { |
120 | 131 | $row = array(); |
— | — | @@ -204,7 +215,6 @@ |
205 | 216 | ); |
206 | 217 | } |
207 | 218 | } |
208 | | - # todo: add class for errors |
209 | 219 | return array( $row ); |
210 | 220 | } |
211 | 221 | |