Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 220 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
FormStore | |
0.00% |
0 / 220 |
|
0.00% |
0 / 5 |
1406 | |
0.00% |
0 / 1 |
setFormData | |
0.00% |
0 / 179 |
|
0.00% |
0 / 1 |
342 | |||
processFormData | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
156 | |||
getWikiName | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getWikiList | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getAdminsList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Store; |
4 | |
5 | use DateTime; |
6 | use DateTimeZone; |
7 | use MediaWiki\Extension\SecurePoll\Context; |
8 | use MediaWiki\Extension\SecurePoll\Crypt\Crypt; |
9 | use MediaWiki\Extension\SecurePoll\Pages\StatusException; |
10 | use MediaWiki\Extension\SecurePoll\Talliers\Tallier; |
11 | use MediaWiki\Registration\ExtensionRegistry; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\WikiMap\WikiMap; |
14 | use MobileContext; |
15 | use RuntimeException; |
16 | |
17 | /** |
18 | * Store for loading the form data. |
19 | */ |
20 | class FormStore extends MemoryStore { |
21 | /** @var int */ |
22 | public $eId; |
23 | /** @var int */ |
24 | public $rId = 0; |
25 | /** @var int[] */ |
26 | public $qIds = []; |
27 | /** @var int[] */ |
28 | public $oIds = []; |
29 | /** @var string[] */ |
30 | public $remoteWikis; |
31 | |
32 | /** @var string */ |
33 | private $lang; |
34 | |
35 | /** |
36 | * @param Context $context |
37 | * @param array $formData |
38 | * @param int $userId |
39 | */ |
40 | public function setFormData( $context, $formData, $userId ) { |
41 | global $wgSecurePollCreateWikiGroupDir, $wgSecurePollCreateWikiGroups, |
42 | $wgSecurePollCreateRemoteScriptPath; |
43 | |
44 | $curId = 0; |
45 | |
46 | $wikis = $formData['property_wiki'] ?? WikiMap::getCurrentWikiId(); |
47 | if ( $wikis === '*' ) { |
48 | $wikis = array_values( self::getWikiList() ); |
49 | } elseif ( substr( $wikis, 0, 1 ) === '@' ) { |
50 | $file = substr( $wikis, 1 ); |
51 | $wikis = false; |
52 | |
53 | // HTMLForm already checked this, but let's do it again anyway. |
54 | if ( isset( $wgSecurePollCreateWikiGroups[$file] ) ) { |
55 | $wikis = file_get_contents( |
56 | $wgSecurePollCreateWikiGroupDir . $file . '.dblist' |
57 | ); |
58 | } |
59 | |
60 | if ( !$wikis ) { |
61 | throw new StatusException( 'securepoll-create-fail-bad-dblist' ); |
62 | } |
63 | $wikis = array_map( 'trim', explode( "\n", trim( $wikis ) ) ); |
64 | } else { |
65 | $wikis = (array)$wikis; |
66 | } |
67 | |
68 | $this->remoteWikis = array_diff( $wikis, [ WikiMap::getCurrentWikiId() ] ); |
69 | |
70 | $cryptTypes = Crypt::getCryptTypes(); |
71 | |
72 | // Create the entry for the election |
73 | [ $ballot, $tally ] = explode( '+', $formData['election_type'] ); |
74 | $ballotTypes = $context->getBallotTypesForVote(); |
75 | if ( !isset( $ballotTypes[$ballot] ) ) { |
76 | // This should not be reachable by normal user input since the |
77 | // ballot type is already validated. |
78 | throw new RuntimeException( 'Invalid ballot type' ); |
79 | } |
80 | $ballotClass = $ballotTypes[$ballot]; |
81 | |
82 | $crypt = $formData['election_crypt']; |
83 | |
84 | $date = new DateTime( |
85 | $formData['election_startdate'], new DateTimeZone( 'GMT' ) |
86 | ); |
87 | $startDate = $date->format( 'YmdHis' ); |
88 | |
89 | $date = new DateTime( |
90 | $formData['election_enddate'], new DateTimeZone( 'GMT' ) |
91 | ); |
92 | $endDate = $date->format( 'YmdHis' ); |
93 | |
94 | $this->lang = $formData['election_primaryLang']; |
95 | |
96 | $eId = (int)$formData['election_id'] <= 0 ? --$curId : (int)$formData['election_id']; |
97 | $this->eId = $eId; |
98 | $this->entityInfo[$eId] = [ |
99 | 'id' => $eId, |
100 | 'type' => 'election', |
101 | 'title' => $formData['election_title'], |
102 | 'ballot' => $ballot, |
103 | 'tally' => $tally, |
104 | 'primaryLang' => $this->lang, |
105 | 'startDate' => wfTimestamp( TS_MW, $startDate ), |
106 | 'endDate' => wfTimestamp( TS_MW, $endDate ), |
107 | 'auth' => $this->remoteWikis ? 'remote-mw' : 'local', |
108 | 'owner' => $userId, |
109 | 'questions' => [], |
110 | ]; |
111 | $this->properties[$eId] = [ |
112 | 'encrypt-type' => $crypt, |
113 | 'wikis' => implode( "\n", $wikis ), |
114 | 'wikis-val' => $formData['property_wiki'] ?? WikiMap::getCurrentWikiId(), |
115 | 'return-url' => $formData['return-url'], |
116 | 'disallow-change' => $formData['disallow-change'] ? 1 : 0, |
117 | 'voter-privacy' => $formData['voter-privacy'] ? 1 : 0, |
118 | 'request-comment' => $formData['request-comment'] ? 1 : 0, |
119 | 'prompt-active-wiki' => $formData['prompt-active-wiki'] ? 1 : 0 |
120 | ]; |
121 | $this->messages[$this->lang][$eId] = [ |
122 | 'title' => $formData['election_title'], |
123 | 'comment-prompt' => $formData['comment-prompt'] |
124 | ]; |
125 | |
126 | $admins = $this->getAdminsList( $formData['property_admins'] ); |
127 | $this->properties[$eId]['admins'] = $admins; |
128 | |
129 | if ( $this->remoteWikis ) { |
130 | $this->properties[$eId]['remote-mw-script-path'] = $wgSecurePollCreateRemoteScriptPath; |
131 | |
132 | $this->rId = $rId = --$curId; |
133 | $this->entityInfo[$rId] = [ |
134 | 'id' => $rId, |
135 | 'type' => 'election', |
136 | 'title' => $formData['election_title'], |
137 | 'ballot' => $ballot, |
138 | 'tally' => $tally, |
139 | 'primaryLang' => $this->lang, |
140 | 'startDate' => wfTimestamp( TS_MW, $startDate ), |
141 | 'endDate' => wfTimestamp( TS_MW, $endDate ), |
142 | 'auth' => 'local', |
143 | 'questions' => [], |
144 | ]; |
145 | $this->properties[$rId]['main-wiki'] = WikiMap::getCurrentWikiId(); |
146 | |
147 | $jumpUrl = SpecialPage::getTitleFor( 'SecurePoll' )->getFullURL(); |
148 | $this->properties[$rId]['jump-url'] = $jumpUrl; |
149 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { |
150 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
151 | $mobileContext = MobileContext::singleton(); |
152 | $this->properties[$rId]['mobile-jump-url'] = |
153 | $mobileContext->getMobileUrl( $jumpUrl ); |
154 | } |
155 | |
156 | $this->properties[$rId]['jump-id'] = $eId; |
157 | $this->properties[$rId]['admins'] = $admins; |
158 | $this->messages[$this->lang][$rId] = [ |
159 | 'title' => $formData['election_title'], |
160 | 'jump-text' => $formData['jump-text'], |
161 | ]; |
162 | } |
163 | |
164 | $this->processFormData( |
165 | $eId, |
166 | $formData, |
167 | $ballotClass, |
168 | 'election' |
169 | ); |
170 | $this->processFormData( |
171 | $eId, |
172 | $formData, |
173 | Tallier::$tallierTypes[$tally], |
174 | 'election' |
175 | ); |
176 | $this->processFormData( |
177 | $eId, |
178 | $formData, |
179 | $cryptTypes[$crypt], |
180 | 'election' |
181 | ); |
182 | |
183 | // Process each question |
184 | foreach ( $formData['questions'] as $question ) { |
185 | if ( (int)$question['id'] <= 0 ) { |
186 | $qId = --$curId; |
187 | } else { |
188 | $qId = (int)$question['id']; |
189 | $this->qIds[] = $qId; |
190 | } |
191 | $this->entityInfo[$qId] = [ |
192 | 'id' => $qId, |
193 | 'type' => 'question', |
194 | 'election' => $eId, |
195 | 'options' => [], |
196 | ]; |
197 | $this->properties[$qId] = []; |
198 | $this->messages[$this->lang][$qId] = [ |
199 | 'text' => $question['text'], |
200 | ]; |
201 | |
202 | $this->processFormData( |
203 | $qId, |
204 | $question, |
205 | $ballotClass, |
206 | 'question' |
207 | ); |
208 | $this->processFormData( |
209 | $qId, |
210 | $question, |
211 | Tallier::$tallierTypes[$tally], |
212 | 'question' |
213 | ); |
214 | $this->processFormData( |
215 | $qId, |
216 | $question, |
217 | $cryptTypes[$crypt], |
218 | 'question' |
219 | ); |
220 | |
221 | // Process options for this question |
222 | foreach ( $question['options'] as $option ) { |
223 | if ( (int)$option['id'] <= 0 ) { |
224 | $oId = --$curId; |
225 | } else { |
226 | $oId = (int)$option['id']; |
227 | $this->oIds[] = $oId; |
228 | } |
229 | $this->entityInfo[$oId] = [ |
230 | 'id' => $oId, |
231 | 'type' => 'option', |
232 | 'election' => $eId, |
233 | 'question' => $qId, |
234 | ]; |
235 | $this->properties[$oId] = []; |
236 | $this->messages[$this->lang][$oId] = [ |
237 | 'text' => $option['text'], |
238 | ]; |
239 | |
240 | $this->processFormData( |
241 | $oId, |
242 | $option, |
243 | $ballotClass, |
244 | 'option' |
245 | ); |
246 | $this->processFormData( |
247 | $oId, |
248 | $option, |
249 | Tallier::$tallierTypes[$tally], |
250 | 'option' |
251 | ); |
252 | $this->processFormData( |
253 | $oId, |
254 | $option, |
255 | $cryptTypes[$crypt], |
256 | 'option' |
257 | ); |
258 | |
259 | $this->entityInfo[$qId]['options'][] = &$this->entityInfo[$oId]; |
260 | } |
261 | |
262 | $this->entityInfo[$eId]['questions'][] = &$this->entityInfo[$qId]; |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * Extract the values for the class's properties and messages |
268 | * |
269 | * @param int $id |
270 | * @param array $formData Form data array |
271 | * @param class-string<Ballot|Crypt|Tallier>|false $class |
272 | * @param string|null $category If given, ::getCreateDescriptors is |
273 | * expected to return an array with subarrays for different categories |
274 | * of descriptors, and this selects which subarray to process. |
275 | */ |
276 | private function processFormData( $id, $formData, $class, $category ) { |
277 | if ( $class === false ) { |
278 | return; |
279 | } |
280 | |
281 | $items = $class::getCreateDescriptors(); |
282 | |
283 | if ( $category ) { |
284 | if ( !isset( $items[$category] ) ) { |
285 | return; |
286 | } |
287 | $items = $items[$category]; |
288 | } |
289 | |
290 | foreach ( $items as $key => $item ) { |
291 | if ( !isset( $item['SecurePoll_type'] ) ) { |
292 | continue; |
293 | } |
294 | $value = $formData[$key]; |
295 | switch ( $item['SecurePoll_type'] ) { |
296 | case 'property': |
297 | $this->properties[$id][$key] = $value; |
298 | break; |
299 | case 'properties': |
300 | foreach ( $value as $k => $v ) { |
301 | $this->properties[$id][$k] = $v; |
302 | } |
303 | break; |
304 | case 'message': |
305 | $this->messages[$this->lang][$id][$key] = $value; |
306 | break; |
307 | case 'messages': |
308 | foreach ( $value ?? [] as $k => $v ) { |
309 | $this->messages[$this->lang][$id][$k] = $v; |
310 | } |
311 | break; |
312 | } |
313 | } |
314 | } |
315 | |
316 | /** |
317 | * Get the name of a wiki |
318 | * |
319 | * @param string $dbname |
320 | * @return string |
321 | */ |
322 | public static function getWikiName( $dbname ) { |
323 | $name = WikiMap::getWikiName( $dbname ); |
324 | |
325 | return $name ?: $dbname; |
326 | } |
327 | |
328 | /** |
329 | * Get the list of wiki names |
330 | * |
331 | * @return array |
332 | */ |
333 | public static function getWikiList() { |
334 | global $wgConf, $wgSecurePollExcludedWikis; |
335 | |
336 | $wikiNames = []; |
337 | foreach ( $wgConf->getLocalDatabases() as $dbname ) { |
338 | |
339 | // SecurePoll is not installed on these |
340 | if ( in_array( $dbname, $wgSecurePollExcludedWikis ) ) { |
341 | continue; |
342 | } |
343 | |
344 | $host = self::getWikiName( $dbname ); |
345 | if ( strpos( $host, '.' ) ) { |
346 | // e.g. "en.wikipedia.org" |
347 | $wikiNames[$host] = $dbname; |
348 | } |
349 | } |
350 | |
351 | // Make sure the local wiki is represented |
352 | $dbname = WikiMap::getCurrentWikiId(); |
353 | $wikiNames[self::getWikiName( $dbname )] = $dbname; |
354 | |
355 | ksort( $wikiNames ); |
356 | |
357 | return $wikiNames; |
358 | } |
359 | |
360 | /** |
361 | * Convert the submitted line-separated string of admin usernames into a |
362 | * pipe-separated string for insertion into the database. |
363 | * |
364 | * @param string $data |
365 | * @return string |
366 | */ |
367 | private function getAdminsList( $data ) { |
368 | return implode( '|', explode( "\n", $data ) ); |
369 | } |
370 | } |