Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 225
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 / 225
0.00% covered (danger)
0.00%
0 / 5
1332
0.00% covered (danger)
0.00%
0 / 1
 setFormData
0.00% covered (danger)
0.00%
0 / 178
0.00% covered (danger)
0.00%
0 / 1
306
 processFormData
0.00% covered (danger)
0.00%
0 / 32
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 ExtensionRegistry;
8use MediaWiki\Extension\SecurePoll\Context;
9use MediaWiki\Extension\SecurePoll\Crypt\Crypt;
10use MediaWiki\Extension\SecurePoll\Pages\StatusException;
11use MediaWiki\Extension\SecurePoll\Talliers\Tallier;
12use MediaWiki\SpecialPage\SpecialPage;
13use MediaWiki\WikiMap\WikiMap;
14use MobileContext;
15
16/**
17 * Store for loading the form data.
18 */
19class 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}