Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 222
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 / 222
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 / 14
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\MediaWikiServices;
12use MediaWiki\Registration\ExtensionRegistry;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\WikiMap\WikiMap;
15use MobileContext;
16use RuntimeException;
17
18/**
19 * Store for loading the form data.
20 */
21class FormStore extends MemoryStore {
22    /** @var int */
23    public $eId;
24    /** @var int */
25    public $rId = 0;
26    /** @var int[] */
27    public $qIds = [];
28    /** @var int[] */
29    public $oIds = [];
30    /** @var string[] */
31    public $remoteWikis;
32
33    /** @var string */
34    private $lang;
35
36    /**
37     * @param Context $context
38     * @param array $formData
39     * @param int $userId
40     */
41    public function setFormData( $context, $formData, $userId ) {
42        $config = MediaWikiServices::getInstance()->getMainConfig();
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( $config->get( 'SecurePollCreateWikiGroups' )[$file] ) ) {
54                $wikis = file_get_contents(
55                    $config->get( 'SecurePollCreateWikiGroupDir' ) . $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            'prompt-active-wiki' => $formData['prompt-active-wiki'] ? 1 : 0
119        ];
120        $this->messages[$this->lang][$eId] = [
121            'title' => $formData['election_title'],
122            'comment-prompt' => $formData['comment-prompt']
123        ];
124
125        $admins = $this->getAdminsList( $formData['property_admins'] );
126        $this->properties[$eId]['admins'] = $admins;
127
128        if ( $this->remoteWikis ) {
129            $this->properties[$eId]['remote-mw-script-path'] =
130                $config->get( 'SecurePollCreateRemoteScriptPath' );
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        // This is a global exception we may want to let it pass.
335        // Even though $wgConf is an instance of MediaWiki\Config\SiteConfiguration,
336        // it’s not exposed as a service, so accessing it via
337        // `MediaWikiServices::getInstance()->getService( 'SiteConfiguration' )` is
338        // not possible.
339        global $wgConf;
340        $securePollExcludedWikis = MediaWikiServices::getInstance()
341            ->getMainConfig()->get( 'SecurePollExcludedWikis' );
342
343        $wikiNames = [];
344        foreach ( $wgConf->getLocalDatabases() as $dbname ) {
345
346            // SecurePoll is not installed on these
347            if ( in_array( $dbname, $securePollExcludedWikis ) ) {
348                continue;
349            }
350
351            $host = self::getWikiName( $dbname );
352            if ( strpos( $host, '.' ) ) {
353                // e.g. "en.wikipedia.org"
354                $wikiNames[$host] = $dbname;
355            }
356        }
357
358        // Make sure the local wiki is represented
359        $dbname = WikiMap::getCurrentWikiId();
360        $wikiNames[self::getWikiName( $dbname )] = $dbname;
361
362        ksort( $wikiNames );
363
364        return $wikiNames;
365    }
366
367    /**
368     * Convert the submitted line-separated string of admin usernames into a
369     * pipe-separated string for insertion into the database.
370     *
371     * @param string $data
372     * @return string
373     */
374    private function getAdminsList( $data ) {
375        return implode( '|', explode( "\n", $data ) );
376    }
377}