Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 220
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
FormStore
0.00% covered (danger)
0.00%
0 / 220
0.00% covered (danger)
0.00%
0 / 5
1406
0.00% covered (danger)
0.00%
0 / 1
 setFormData
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 1
342
 processFormData
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
156
 getWikiName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getWikiList
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getAdminsList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Store;
4
5use DateTime;
6use DateTimeZone;
7use MediaWiki\Extension\SecurePoll\Context;
8use MediaWiki\Extension\SecurePoll\Crypt\Crypt;
9use MediaWiki\Extension\SecurePoll\Pages\StatusException;
10use MediaWiki\Extension\SecurePoll\Talliers\Tallier;
11use MediaWiki\Registration\ExtensionRegistry;
12use MediaWiki\SpecialPage\SpecialPage;
13use MediaWiki\WikiMap\WikiMap;
14use MobileContext;
15use RuntimeException;
16
17/**
18 * Store for loading the form data.
19 */
20class 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}