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