r98404 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r98403‎ | r98404 | r98405 >
Date:07:59, 29 September 2011
Author:questpc
Status:deferred (Comments)
Tags:
Comment:
Separate question result views. Structured answer is stored in DB and is assessible in interpretation scripts. Control of answer storage from interpretation scripts. Proposal names are implemented and may be used in interpretation scripts.
Modified paths:
  • /trunk/extensions/QPoll/clientside/qp_results.css (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_poll.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/qp_interpresult.php (added) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php (modified) (history)
  • /trunk/extensions/QPoll/ctrl/question/qp_textquestion.php (modified) (history)
  • /trunk/extensions/QPoll/i18n/qp.i18n.php (modified) (history)
  • /trunk/extensions/QPoll/includes/qp_excel.php (added) (history)
  • /trunk/extensions/QPoll/interpretation (added) (history)
  • /trunk/extensions/QPoll/interpretation/qp_eval.php (added) (history)
  • /trunk/extensions/QPoll/interpretation/qp_interpret.php (added) (history)
  • /trunk/extensions/QPoll/model (added) (history)
  • /trunk/extensions/QPoll/model/qp_pollstore.php (added) (history)
  • /trunk/extensions/QPoll/model/qp_question_collection.php (added) (history)
  • /trunk/extensions/QPoll/model/qp_questiondata.php (added) (history)
  • /trunk/extensions/QPoll/qp_eval.php (deleted) (history)
  • /trunk/extensions/QPoll/qp_interpret.php (deleted) (history)
  • /trunk/extensions/QPoll/qp_pollstore.php (deleted) (history)
  • /trunk/extensions/QPoll/qp_question_collection.php (deleted) (history)
  • /trunk/extensions/QPoll/qp_questiondata.php (deleted) (history)
  • /trunk/extensions/QPoll/qp_user.php (modified) (history)
  • /trunk/extensions/QPoll/specials/qp_results.php (modified) (history)
  • /trunk/extensions/QPoll/specials/qp_webinstall.php (modified) (history)
  • /trunk/extensions/QPoll/view/poll/qp_pollview.php (modified) (history)
  • /trunk/extensions/QPoll/view/proposal/qp_stubquestionproposalview.php (modified) (history)
  • /trunk/extensions/QPoll/view/qp_interpresultview.php (added) (history)
  • /trunk/extensions/QPoll/view/question/qp_tabularquestionview.php (modified) (history)
  • /trunk/extensions/QPoll/view/question/qp_textquestionview.php (modified) (history)
  • /trunk/extensions/QPoll/view/results (added) (history)
  • /trunk/extensions/QPoll/view/results/qp_questiondataresults.php (added) (history)
  • /trunk/extensions/QPoll/view/results/qp_textquestiondataresults.php (added) (history)

Diff [purge]

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( " ", "&#8194;", 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 @@
4747 */
4848 $messages['en'] = array(
4949 'pollresults' => 'Results of the polls on this site',
 50+ 'qpollwebinstall' => 'Installation / update of QPoll extension',
5051 'qp_parentheses' => '($1)',
5152 'qp_full_category_name' => '$1($2)',
5253 'qp_desc' => 'Allows creation of polls',
Index: trunk/extensions/QPoll/clientside/qp_results.css
@@ -1,11 +1,12 @@
22 .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; }
1011 .qpoll .cat_part { background-color: Lightyellow; border: 1px solid gray; padding: 0 0.5em 0 0.5em; }
1112 .qpoll .cat_unknown { color: white; background-color: IndianRed; border: 1px solid gray; padding: 0 0.5em 0 0.5em; }
1213 .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
1186 + 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
1194 + 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
1922 + native
Index: trunk/extensions/QPoll/specials/qp_results.php
@@ -58,7 +58,7 @@
5959 * @param $user User: the user to check
6060 * @return Boolean: does the user have permission to view the page?
6161 */
62 - public function userCanExecute( User $user ) {
 62+ public function userCanExecute( $user ) {
6363 # this fn is used to decide whether to show the page link at Special:Specialpages
6464 foreach ( self::$accessPermissions as $permission ) {
6565 if ( !$user->isAllowed( $permission ) ) {
@@ -230,8 +230,9 @@
231231 foreach ( $pollStore->Questions as &$qdata ) {
232232 if ( $pollStore->isUsedQuestion( $qdata->question_id ) ) {
233233 $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 );
236237 }
237238 }
238239 return $output;
@@ -252,7 +253,9 @@
253254 $output .= $this->qpLink( $this->getTitle(), wfMsg( 'qp_export_to_xls' ), array( "style" => "font-weight:bold;" ), array( 'action' => 'stats_xls', 'id' => $pid ) ) . "<br />\n";
254255 $output .= $this->qpLink( $this->getTitle(), wfMsg( 'qp_voices_to_xls' ), array( "style" => "font-weight:bold;" ), array( 'action' => 'voices_xls', 'id' => $pid ) ) . "<br />\n";
255256 foreach ( $pollStore->Questions as &$qdata ) {
256 - $output .= $qdata->displayQuestionStats( $this, $pid );
 257+ $qview = $qdata->createView();
 258+ $output .= $qview->displayQuestionStats( $this, $pid );
 259+ unset( $qview );
257260 }
258261 }
259262 }
@@ -481,9 +484,11 @@
482485 return "<div>" . self::$PollsLink . "</div>\n";
483486 }
484487
485 -}
 488+} /* end of PollResults class */
486489
487 -/* list of all users */
 490+/**
 491+ * List all users
 492+ */
488493 class qp_UsersList extends qp_QueryPage {
489494 var $cmd;
490495 var $order_by;
@@ -545,9 +550,11 @@
546551 return PollResults::getPollsLink() . '<div class="head">' . wfMsg( 'qp_users_list' ) . '<div>' . $this->different_order_by_link . '</div></div>';
547552 }
548553
549 -}
 554+} /* end of qp_UsersList class */
550555
551 -/* list of polls in which selected user (did not|participated) */
 556+/**
 557+ * List of polls in which selected user has (not) participated
 558+ */
552559 class qp_UserPollsList extends qp_QueryPage {
553560 var $uid;
554561 var $inverse;
@@ -623,9 +630,11 @@
624631 return $params;
625632 }
626633
627 -}
 634+} /* end of qp_UserPollsList class */
628635
629 -/* list of all polls */
 636+/**
 637+ * List all polls
 638+ */
630639 class qp_PollsList extends qp_QueryPage {
631640
632641 function getIntervalResults( $offset, $limit ) {
@@ -648,7 +657,7 @@
649658
650659 function formatResult( $result ) {
651660 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, '' ) );
653662 $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) );
654663 $pollname = qp_Setup::specialchars( $result->poll_id );
655664 $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) );
@@ -663,9 +672,11 @@
664673 return PollResults::getUsersLink() . '<div class="head">' . wfMsg( 'qp_polls_list' ) . '</div>';
665674 }
666675
667 -}
 676+} /* end of qp_PollsList class */
668677
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+ */
670681 class qp_PollUsersList extends qp_QueryPage {
671682
672683 var $pid;
@@ -689,7 +700,7 @@
690701 'page_id=article_id and pid=' . $db->addQuotes( $this->pid ),
691702 __METHOD__ );
692703 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, '' ) );
694705 $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) );
695706 $pollname = qp_Setup::specialchars( $row->poll_id );
696707 $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) );
@@ -743,9 +754,11 @@
744755 return $params;
745756 }
746757
747 -}
 758+} /* end of qp_PollUsersList class */
748759
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+ */
750763 class qp_UserCellList extends qp_QueryPage {
751764 var $cmd;
752765 var $pid = null;
@@ -782,7 +795,7 @@
783796 $pollStore = new qp_PollStore( array( 'from' => 'pid', 'pid' => $this->pid ) );
784797 if ( $pollStore->pid !== null ) {
785798 $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, '' ) );
787800 $pagename = qp_Setup::specialchars( $wgContLang->convert( $poll_title->getPrefixedText() ) );
788801 $pollname = qp_Setup::specialchars( $this->poll_id );
789802 $goto_link = $this->qpLink( $poll_title, wfMsg( 'qp_source_link' ) );
@@ -865,31 +878,4 @@
866879 return $params;
867880 }
868881
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 @@
1717 parent::__construct( 'QPollWebInstall', 'read' );
1818 }
1919
 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+
2029 public function execute( $par ) {
2130 global $wgOut, $wgUser;
2231
2332 # only sysops and bureaucrats can update the DB
24 - if ( count( array_intersect( $this->allowed_groups, $wgUser->getEffectiveGroups() ) ) == 0 ) {
 33+ if ( !$this->userCanExecute( $wgUser ) ) {
2534 $wgOut->addHTML( 'You have to be a member of the following group(s) to perform web install:' . implode( ', ', $this->allowed_groups ) );
2635 return;
2736 }
Index: trunk/extensions/QPoll/ctrl/poll/qp_abstractpoll.php
@@ -40,7 +40,8 @@
4141 /**
4242 * A poll stub controller which cannot process and render itself
4343 * 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()
4546 * $this->parseInput()
4647 * $this->view->renderPoll()
4748 */
@@ -106,7 +107,7 @@
107108 $view->setPerRow( $perRow );
108109 $this->view = $view;
109110 # 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 )
111112 $this->username = qp_Setup::getCurrUserName();
112113 # setup poll view showresults
113114 if ( array_key_exists( 'showresults', $argv ) && qp_Setup::$global_showresults != 0 ) {
@@ -134,10 +135,14 @@
135136 * @param $input Text between <qpoll> and </qpoll> tags, in QPoll syntax.
136137 */
137138 function parsePoll( $input ) {
138 - if ( ( $result = $this->getPollStore() ) !== true ) {
 139+ if ( ( $result = $this->setHeaders() ) !== true ) {
139140 # error message box (invalid poll attributes)
140141 return $result;
141142 }
 143+ if ( ( $result = $this->getPollStore() ) !== true ) {
 144+ # error message box (cannot load from store)
 145+ return $result;
 146+ }
142147 if ( $this->parseInput( $input ) === true ) {
143148 # no output generation - due to active redirect or access denied
144149 return '';
Index: trunk/extensions/QPoll/ctrl/poll/qp_pollstats.php
@@ -47,9 +47,12 @@
4848 $this->pollAddr = trim( $argv['address'] );
4949 }
5050
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() {
5457 if ( $this->mPollId !== null ) {
5558 $this->mState = "error";
5659 return self::fatalError( 'qp_error_id_in_stats_mode' );
@@ -58,6 +61,15 @@
5962 $this->mState = "error";
6063 return self::fatalError( 'qp_error_dependance_in_stats_mode' );
6164 }
 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() {
6274 $this->pollStore = qp_PollStore::newFromAddr( $this->pollAddr );
6375 if ( !( $this->pollStore instanceof qp_PollStore ) || $this->pollStore->pid === null ) {
6476 return self::fatalError( 'qp_error_no_such_poll', $this->pollAddr );
Index: trunk/extensions/QPoll/ctrl/poll/qp_poll.php
@@ -59,6 +59,9 @@
6060 # dependance attr
6161 if ( array_key_exists( 'dependance', $argv ) ) {
6262 $this->dependsOn = trim( $argv['dependance'] );
 63+ if ( $this->dependsOn === 'dependance' ) {
 64+ $this->dependsOn = '';
 65+ }
6366 }
6467 # interpretation attr
6568 if ( array_key_exists( 'interpretation', $argv ) ) {
@@ -97,10 +100,12 @@
98101 $this->mBeingCorrected = ( $this->mRequest->getVal( 'pollId' ) == $this->mPollId );
99102 }
100103
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() {
105110 if ( $this->mPollId == null ) {
106111 $this->mState = "error";
107112 return self::fatalError( 'qp_error_no_poll_id' );
@@ -120,12 +125,17 @@
121126 }
122127 if ( $this->dependsOn != '' ) {
123128 $depsOnAddr = self::getPrefixedPollAddress( $this->dependsOn );
124 - if ( is_array( $depsOnAddr ) ) {
125 - $this->dependsOn = $depsOnAddr[2];
126 - } else {
 129+ if ( !is_array( $depsOnAddr ) ) {
127130 return self::fatalError( 'qp_error_invalid_dependance_value', $this->mPollId, $this->dependsOn );
128131 }
 132+ $this->dependsOn = $depsOnAddr[2];
129133 }
 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() {
130140 $newPollStore = array(
131141 'poll_id' => $this->mPollId,
132142 'order_id' => $this->mOrderId,
@@ -240,66 +250,65 @@
241251 # @return true when dependance is fulfilled, error message otherwise
242252 private function checkDependance( $dependsOn, $nonVotedDepLink = false ) {
243253 # 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;
268276 } 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 );
270279 }
271280 } 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 );
281282 }
282283 } 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 {
286308 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' );
300309 }
 310+ default :
 311+ throw new MWException( __METHOD__ . ' invalid dependance poll store found' );
301312 }
302 - } else {
303 - return true;
304313 }
305314 }
306315
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
1113 + native
Index: trunk/extensions/QPoll/ctrl/question/qp_abstractquestion.php
@@ -22,10 +22,11 @@
2323 # some questions has a subtype; currently is not stored in DB;
2424 # should always be properly initialized in parent controller via $poll->parseMainHeader()
2525 var $mSubType = '';
26 - var $mCategories = Array();
27 - var $mCategorySpans = Array();
 26+ var $mCategories = array();
 27+ var $mCategorySpans = array();
2828 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
3031 var $alreadyVoted = false; // whether the selected user has already voted this question ?
3132
3233 # statistics
@@ -92,6 +93,10 @@
9394 $this->view->setPropWidth( $paramkeys[ 'propwidth' ] );
9495 }
9596
 97+ function getProposalIdByName( $proposalName ) {
 98+ return array_search( $proposalName, $this->mProposalNames, true );
 99+ }
 100+
96101 function getPercents( $proposalId, $catId ) {
97102 if ( is_array( $this->Percents ) && array_key_exists( $proposalId, $this->Percents ) &&
98103 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 @@
176176 $proposalId = 0;
177177 # Currently, we use just a single instance (no nested categories)
178178 $opt = new qp_TextQuestionOptions();
 179+ # set static view state for the future qp_TextQuestionProposalView instances
179180 qp_TextQuestionProposalView::applyViewState( $this->view );
180181 foreach ( $this->raws as $raw ) {
181182 $opt->reset();
182183 $this->propview = new qp_TextQuestionProposalView( $proposalId, $this );
 184+ # set proposal name (if any)
 185+ $prop_name = qp_QuestionData::splitRawProposal( $raw );
183186 $this->dbtokens = $brace_stack = array();
184187 $catId = 0;
185188 $last_brace = '';
@@ -245,13 +248,18 @@
246249 # todo: this is the explanary line, it is not real proposal
247250 $this->propview->prependErrorToken( wfMsg( 'qp_error_too_few_categories' ), 'error' );
248251 }
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 ) {
250255 # too long proposal field to store into the DB
251256 # this is very important check for text questions because
252257 # category definitions are stored within the proposal text
253258 $this->propview->prependErrorToken( wfMsg( 'qp_error_too_long_proposal_text' ), 'error' );
254259 }
255260 $this->mProposalText[$proposalId] = $proposal_text;
 261+ if ( $prop_name !== '' ) {
 262+ $this->mProposalNames[$proposalId] = $prop_name;
 263+ }
256264 if ( $this->poll->mBeingCorrected ) {
257265 # check for unanswered categories
258266 try {
Index: trunk/extensions/QPoll/ctrl/question/qp_mixedquestion.php
@@ -21,6 +21,7 @@
2222 }
2323 $this->mProposalPattern .= '(.*)`u';
2424 $proposalId = -1;
 25+ # set static view state for the future qp_TabularQuestionProposalView instances
2526 qp_TabularQuestionProposalView::applyViewState( $this->view );
2627 foreach ( $this->raws as $raw ) {
2728 # new proposal view
@@ -40,7 +41,11 @@
4142 continue;
4243 }
4344 $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 );
4550 # Determine a type ID, according to the questionType and the number of signes.
4651 foreach ( $this->mCategories as $catId => $catDesc ) {
4752 $typeId = $matches[ $catId ];
Index: trunk/extensions/QPoll/ctrl/question/qp_tabularquestion.php
@@ -271,6 +271,7 @@
272272 */
273273 function questionParseBody( $inputType ) {
274274 $proposalId = -1;
 275+ # set static view state for the future qp_TabularQuestionProposalView instances
275276 qp_TabularQuestionProposalView::applyViewState( $this->view );
276277 foreach ( $this->raws as $raw ) {
277278 if ( !preg_match( $this->mProposalPattern, $raw, $matches ) ) {
@@ -280,7 +281,11 @@
281282 $pview = new qp_TabularQuestionProposalView( $proposalId + 1, $this );
282283 $proposalId++;
283284 $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 );
285290 foreach ( $this->mCategories as $catId => $catDesc ) {
286291 # start new input field tag (category)
287292 $pview->addNewCategory( $catId );
Index: trunk/extensions/QPoll/ctrl/question/qp_stubquestion.php
@@ -98,6 +98,7 @@
9999 'categories' => $this->mCategories,
100100 'category_spans' => $this->mCategorySpans,
101101 'proposal_text' => $this->mProposalText,
 102+ 'proposal_names' => $this->mProposalNames,
102103 'proposal_category_id' => $this->mProposalCategoryId,
103104 'proposal_category_text' => $this->mProposalCategoryText ) );
104105 }
Index: trunk/extensions/QPoll/qp_user.php
@@ -89,7 +89,7 @@
9090 if ( count( $args ) < 1 ) {
9191 return;
9292 }
93 - $message = $args[0];
 93+ $message = strval( $args[0] );
9494 $debug = true;
9595 if ( count( $args ) > 2 ) {
9696 $debug = $args[2];
@@ -116,6 +116,26 @@
117117 }
118118
119119 /**
 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+/**
120140 * Extension's global settings and initializiers
121141 * should be purely static and preferrably have no constructor
122142 */
@@ -206,9 +226,9 @@
207227 public static $structured_interpretation_max_length = 65535;
208228 # whether to show short, long, structured interpretation results to end user
209229 public static $show_interpretation = array(
210 - 'short' => false,
 230+ 'short' => true,
211231 'long' => true,
212 - 'structured' => false
 232+ 'structured' => true
213233 );
214234 /* end of default configuration settings */
215235
@@ -267,13 +287,11 @@
268288 'qp_user.php' => 'qp_Setup',
269289 'includes/qp_functionshook.php' => 'qp_FunctionsHook',
270290 'includes/qp_renderer.php' => 'qp_Renderer',
 291+ 'includes/qp_excel.php' => 'qp_Excel',
271292
272293 ## DB schema updater
273294 'maintenance/qp_schemaupdater.php' => 'qp_SchemaUpdater',
274295
275 - ## collection of the questions
276 - 'qp_question_collection.php' => 'qp_QuestionCollection',
277 -
278296 ## controllers (polls and questions derived from separate abstract classes)
279297 # polls
280298 'ctrl/poll/qp_abstractpoll.php' => 'qp_AbstractPoll',
@@ -286,6 +304,8 @@
287305 'ctrl/question/qp_mixedquestion.php' => 'qp_MixedQuestion',
288306 'ctrl/question/qp_textquestion.php' => array( 'qp_TextQuestionOptions', 'qp_TextQuestion' ),
289307 'ctrl/question/qp_questionstats.php' => 'qp_QuestionStats',
 308+ # interpretation results
 309+ 'ctrl/qp_interpresult.php' => 'qp_InterpResult',
290310
291311 # generic view
292312 'view/qp_abstractview.php' => 'qp_AbstractView',
@@ -300,28 +320,34 @@
301321 'view/question/qp_tabularquestionview.php' => 'qp_TabularQuestionView',
302322 'view/question/qp_textquestionview.php' => 'qp_TextQuestionView',
303323 '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
305325 # proposals
306326 'view/proposal/qp_stubquestionproposalview.php' => 'qp_StubQuestionProposalView',
307327 'view/proposal/qp_tabularquestionproposalview.php' => 'qp_TabularQuestionProposalView',
308328 'view/proposal/qp_questionstatsproposalview.php' => 'qp_QuestionStatsProposalView',
309329 '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',
310335
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',
313343
314 - # question storage and page result question views
315 - # (combined question storage & view)
316 - 'qp_questiondata.php' => array( 'qp_QuestionData', 'qp_TextQuestionData' ),
317 -
318344 # special pages
319345 'specials/qp_special.php' => array( 'qp_SpecialPage', 'qp_QueryPage' ),
320346 'specials/qp_results.php' => 'PollResults',
321347 'specials/qp_webinstall.php' => array( 'qp_WebInstall' ),
322348
323349 # 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'
326352 ) );
327353
328354 # 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
174 + 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
1167 + 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
1419 + native
Index: trunk/extensions/QPoll/view/proposal/qp_stubquestionproposalview.php
@@ -8,6 +8,7 @@
99
1010 # proposal's id
1111 var $proposalId;
 12+
1213 # an instance of question's controller
1314 var $ctrl;
1415 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
152 + 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( " ", "&#8194;", 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
1124 + 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
171 + native
Index: trunk/extensions/QPoll/view/poll/qp_pollview.php
@@ -92,17 +92,17 @@
9393 */
9494 function renderPoll() {
9595 global $wgOut, $wgRequest;
 96+ $pollStore = $this->ctrl->pollStore;
9697 # Generates the output.
9798 $qpoll_div = array( '__tag' => 'div', 'class' => 'qpoll' );
9899 $qpoll_div[] = array( '__tag' => 'a', 'name' => $this->ctrl->getPollTitleFragment( null, '' ), 0 => '' );
99100 # 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 );
107107 # create voting form and fill it with messages and inputs
108108 $qpoll_form = array( '__tag' => 'form', 'method' => 'post', 'action' => $this->ctrl->getPollTitleFragment(), 'autocomplete' => 'off', '__end' => "\n" );
109109 $qpoll_div[] = &$qpoll_form;
@@ -124,15 +124,15 @@
125125 $qpoll_form[] = array( '__tag' => 'div', 'class' => 'pollQuestions', 0 => $this->renderQuestionViews() );
126126 $submitBtn = array( '__tag' => 'input', 'type' => 'submit' );
127127 $submitMsg = 'qp_vote_button';
128 - if ( $this->ctrl->pollStore->isAlreadyVoted() ) {
 128+ if ( $pollStore->isAlreadyVoted() ) {
129129 $submitMsg = 'qp_vote_again_button';
130130 }
131131 if ( $this->ctrl->mBeingCorrected ) {
132 - if ( $this->ctrl->pollStore->getState() == "complete" ) {
 132+ if ( $pollStore->getState() == "complete" ) {
133133 $submitMsg = 'qp_vote_again_button';
134134 }
135135 } else {
136 - if ( $this->ctrl->pollStore->getState() == "error" ) {
 136+ if ( $pollStore->getState() == "error" ) {
137137 $submitBtn[ 'disabled' ] = 'disabled';
138138 }
139139 }
Index: trunk/extensions/QPoll/view/question/qp_tabularquestionview.php
@@ -226,7 +226,16 @@
227227 # there is no interpretation error
228228 return;
229229 }
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+ }
231240 if ( isset( $this->pviews[$prop_id] ) ) {
232241 # the whole proposal line has errors
233242 $propview = &$this->pviews[$prop_id];
Index: trunk/extensions/QPoll/view/question/qp_textquestionview.php
@@ -86,7 +86,16 @@
8787 # there is no interpretation error
8888 return;
8989 }
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+ }
91100 if ( isset( $this->pviews[$prop_id] ) ) {
92101 # the whole proposal line has errors
93102 $propview = &$this->pviews[$prop_id];
@@ -113,7 +122,9 @@
114123 }
115124
116125 /**
117 - *
 126+ * Generates tagarray representation from the list of viewtokens
 127+ * @param $viewtokens array of viewtokens
 128+ * @return tagarray
118129 */
119130 function renderParsedProposal( &$viewtokens ) {
120131 $row = array();
@@ -204,7 +215,6 @@
205216 );
206217 }
207218 }
208 - # todo: add class for errors
209219 return array( $row );
210220 }
211221

Comments

#Comment by Siebrand (talk | contribs)   15:58, 29 September 2011

Please add message documentation for the newly added messages. Thanks.

#Comment by QuestPC (talk | contribs)   05:37, 30 September 2011

Explained few messages. Maybe not ideal but due to severe lack of time cannot do better right now.

Status & tagging log