Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportCsv
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 18
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 validateRunId
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 importFromFile
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 importFromArray
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 isValidQId
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getInputHash
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 addValidatedResult
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getCsvColumnHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 processInput
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 deleteRun
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getWarnings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isOverwrite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOverwrite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRunId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\User\UserIdentity;
5
6class ImportCsv {
7
8    /**
9     * @var string[]
10     */
11    private static $columnHeaders = [ 'queryId', 'formulaId' ];
12    /**
13     * @var bool|int|string
14     */
15    private $runId = false;
16    /**
17     * @var string[]
18     */
19    private $warnings = [];
20    /**
21     * @var array[]
22     */
23    private $results = [];
24    /**
25     * @var bool[] Array mapping numeric qId values to either true (exists) or false (doesn't exist)
26     */
27    private $validQIds = [];
28    /**
29     * @var bool
30     */
31    private $overwrite = false;
32    private UserIdentity $user;
33
34    function __construct( UserIdentity $user ) {
35        $this->user = $user;
36    }
37
38    /**
39     * @param resource $csvFile
40     * @param int|null $runId
41     * @param bool $overwrite
42     *
43     * @return string|true|null
44     */
45    public function execute( $csvFile, $runId = null, $overwrite = false ) {
46        $this->overwrite = $overwrite;
47        $runId = $this->validateRunId( $runId );
48        if ( $runId !== false ) {
49            $success = $this->importFromFile( $csvFile );
50            if ( $success == true ) {
51                return $this->processInput();
52            }
53        } else {
54            return "Error: Invalid runId.";
55        }
56    }
57
58    /**
59     * @param string $run
60     * @return bool|int|string
61     */
62    function validateRunId( $run ) {
63        if ( $run == '' ) {
64            return date( 'Y-m-d H:i:s (e)' );
65        }
66        $dbw = MediaWikiServices::getInstance()
67            ->getConnectionProvider()
68            ->getPrimaryDatabase();
69        $uID = $this->getUser()->getId();
70        if ( is_int( $run ) ) {
71            $runId = $dbw->selectField( 'math_wmc_runs', 'runId',
72                [ 'isDraft' => true, 'userID' => $uID, 'runId' => $run ] );
73        } else {
74            $runId = $dbw->selectField( 'math_wmc_runs', 'runId',
75                [ 'isDraft' => true, 'userID' => $uID, 'runName' => $run ] );
76        }
77        if ( !$runId ) {
78            $exists = $dbw->selectField( 'math_wmc_runs', 'runId',
79                    [ 'userID' => $uID, 'runName' => $run ] );
80            if ( !$exists ) {
81                $success = $dbw->insert( 'math_wmc_runs',
82                        [ 'isDraft' => true, 'userID' => $uID, 'runName' => $run ] );
83                if ( $success ) {
84                    $this->runId = $dbw->insertId();
85                    $this->warnings[] = wfMessage( 'math-wmc-RunAdded', $run, $this->runId )->text();
86                } else {
87                    $this->runId = false;
88                    $this->warnings[] = wfMessage( 'math-wmc-RunAddError', $run )->text();
89                }
90            } else {
91                $this->warnings[] = wfMessage( 'math-wmc-RunAddExist', $run, $exists )->text();
92                $this->runId = false;
93            }
94        } else {
95            $this->runId = $runId;
96        }
97        return $this->runId;
98    }
99
100    public function getUser(): UserIdentity {
101        return $this->user;
102    }
103
104    public function setUser( UserIdentity $user ) {
105        $this->user = $user;
106    }
107
108    /**
109     * @param resource|null $csv_file
110     * @return null
111     * @throws Exception
112     */
113    public function importFromFile( $csv_file ) {
114        if ( $csv_file === null ) {
115            return wfMessage( 'emptyfile' )->text();
116        }
117
118        $table = [];
119
120        $line = fgetcsv( $csv_file );
121        if ( $line === null ) {
122            throw new Exception( "Problem processing the csv file." );
123        }
124        while ( $line !== false ) {
125            array_push( $table, $line );
126            $line = fgetcsv( $csv_file );
127        }
128        fclose( $csv_file );
129
130        // Get rid of the "byte order mark", if it's there - this is
131        // a three-character string sometimes put at the beginning
132        // of files to indicate its encoding.
133        // Code copied from:
134        // http://www.dotvoid.com/2010/04/detecting-utf-bom-byte-order-mark/
135        $byteOrderMark = pack( "CCC", 0xef, 0xbb, 0xbf );
136        if ( strncmp( $table[0][0], $byteOrderMark, 3 ) == 0 ) {
137            $table[0][0] = substr( $table[0][0], 3 );
138            // If there were quotation marks around this value,
139            // they didn't get removed, so remove them now.
140            $table[0][0] = trim( $table[0][0], '"' );
141        }
142        return $this->importFromArray( $table );
143    }
144
145    /**
146     * @param string[] $table
147     * @return null|string
148     */
149    public function importFromArray( $table ) {
150        global $wgMathWmcMaxResults;
151
152        // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName
153        define( 'ImportPattern', '/math\.(\d+)\.(\d+)/' );
154
155        // check header line
156        $uploadedHeaders = $table[0];
157        if ( $uploadedHeaders != self::$columnHeaders ) {
158            return wfMessage( 'math-wmc-bad-header' )->text();
159        }
160        $rank = 0;
161        $lastQueryID = 0;
162        foreach ( $table as $i => $line ) {
163            if ( $i == 0 ) {
164                continue;
165            }
166            $pId = false;
167            $eId = false;
168            $fHash = false;
169            $qId = trim( $line[0] );
170            $fId = trim( $line[1] );
171            $qValid = $this->isValidQId( $qId );
172            if ( $qValid == false ) {
173                $this->warnings[] = wfMessage( 'math-wmc-wrong-query-reference', $i, $qId )->text();
174            }
175            if ( preg_match( ImportPattern, $fId, $m ) ) {
176                $pId = (int)$m[1];
177                $eId = (int)$m[2];
178                $fHash = $this->getInputHash( $pId, $eId );
179                if ( $fHash == false ) {
180                    $this->warnings[] = wfMessage( 'math-wmc-wrong-formula-reference', $i, $pId, $eId )->text();
181                }
182            } else {
183                $this->warnings[] = wfMessage( 'math-wmc-malformed-formula-reference', $i, $fId,
184                    ImportPattern )->text();
185            }
186            if ( $qValid === true && $fHash !== false ) {
187                // a valid result has been submitted
188                if ( $qId == $lastQueryID ) {
189                    $rank++;
190                } else {
191                    $lastQueryID = $qId;
192                    $rank = 1;
193                }
194                if ( $rank <= $wgMathWmcMaxResults ) {
195                    $this->addValidatedResult( $qId, $pId, $eId, $fHash, $rank );
196                } else {
197                    $this->warnings[] = wfMessage( 'math-wmc-too-many-results', $i, $qId, $fId, $rank,
198                        $wgMathWmcMaxResults )->text();
199                }
200            }
201        }
202        return true;
203    }
204
205    /**
206     * @param int $qId
207     * @return bool
208     */
209    private function isValidQId( $qId ) {
210        if ( !array_key_exists( $qId, $this->validQIds ) ) {
211            $dbr = MediaWikiServices::getInstance()
212                ->getConnectionProvider()
213                ->getReplicaDatabase();
214            $exists = (bool)$dbr->selectField( 'math_wmc_ref', 'qId', [ 'qId' => $qId ] );
215            $this->validQIds[$qId] = $exists;
216        }
217        return $this->validQIds[$qId];
218    }
219
220    /**
221     * @param int $pId
222     * @param string $eId
223     * @return string|false
224     */
225    private function getInputHash( $pId, $eId ) {
226        $dbr = MediaWikiServices::getInstance()
227            ->getConnectionProvider()
228            ->getReplicaDatabase();
229        return $dbr->selectField( 'mathindex', 'mathindex_inputhash',
230            [ 'mathindex_revision_id' => $pId, 'mathindex_anchor' => $eId ] );
231    }
232
233    /**
234     * @param int $qId
235     * @param int $pId
236     * @param string $eId
237     * @param string $fHash
238     * @param int $rank
239     */
240    private function addValidatedResult( $qId, $pId, $eId, $fHash, $rank ) {
241        $this->results[] = [
242            'runId' => $this->runId,
243            'qId' => $qId,
244            'oldId' => $pId,
245            'fId' => $eId,
246            'rank' => $rank,
247            'math_inputhash' => $fHash
248        ];
249    }
250
251    /**
252     * @return string
253     */
254    public static function getCsvColumnHeader() {
255        return implode( ',', self::$columnHeaders );
256    }
257
258    /**
259     * @return true
260     */
261    function processInput() {
262        $this->deleteRun( $this->runId );
263        $dbw = MediaWikiServices::getInstance()
264            ->getConnectionProvider()
265            ->getPrimaryDatabase();
266        $dbw->begin( __METHOD__ );
267        foreach ( $this->results as $result ) {
268            $dbw->insert( 'math_wmc_results', $result );
269        }
270        $dbw->commit( __METHOD__ );
271        return true;
272    }
273
274    /**
275     * @param string $runID
276     */
277    public function deleteRun( $runID ) {
278        if ( $this->overwrite ) {
279            $dbw = MediaWikiServices::getInstance()
280                ->getConnectionProvider()
281                ->getPrimaryDatabase();
282            $dbw->delete( 'math_wmc_results', [ 'runId' => $runID ] );
283        }
284    }
285
286    /**
287     * @return string[]
288     */
289    public function getWarnings() {
290        return $this->warnings;
291    }
292
293    /**
294     * @return bool
295     */
296    public function isOverwrite() {
297        return $this->overwrite;
298    }
299
300    /**
301     * @param bool $overwrite
302     */
303    public function setOverwrite( $overwrite ) {
304        $this->overwrite = $overwrite;
305    }
306
307    /**
308     * @return array[]
309     */
310    public function getResults() {
311        return $this->results;
312    }
313
314    /**
315     * @return int
316     */
317    public function getRunId() {
318        return $this->runId;
319    }
320
321}