Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.25% covered (danger)
4.25%
9 / 212
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathSearchHooks
4.25% covered (danger)
4.25%
9 / 212
0.00% covered (danger)
0.00%
0 / 18
2244.93
0.00% covered (danger)
0.00%
0 / 1
 onLoadExtensionSchemaUpdates
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 updateIndex
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 setMathId
8.33% covered (danger)
8.33%
1 / 12
0.00% covered (danger)
0.00%
0 / 1
24.26
 updateMathIndex
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 addIdentifierDescription
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 addLinkToFormulaInfoPage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 onMathFormulaRenderedNoLink
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 generateMathAnchorString
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 writeMathIndex
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 mQueryTagHook
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 onArticleDeleteComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onArticleUndelete
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 onPageContentSaveComplete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 registerExtension
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getRevIdGenerator
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMwsHarvest
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3use MediaWiki\Extension\Math\MathLaTeXML;
4use MediaWiki\Extension\Math\MathRenderer;
5use MediaWiki\Installer\DatabaseUpdater;
6use MediaWiki\Logger\LoggerFactory;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Parser\Parser;
9use MediaWiki\Revision\RevisionRecord;
10use MediaWiki\Revision\SlotRecord;
11use MediaWiki\SpecialPage\SpecialPage;
12use MediaWiki\Status\Status;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15use Wikimedia\Rdbms\DBConnRef;
16
17/**
18 * MediaWiki MathSearch extension
19 *
20 * (c) 2012 various MediaWiki contributors
21 * GPLv2 license; info in main package.
22 */
23class MathSearchHooks {
24
25    /** @var MathIdGenerator[] */
26    private static $idGenerators = [];
27
28    /**
29     * LoadExtensionSchemaUpdates handler; set up math table on install/upgrade.
30     *
31     * @param DatabaseUpdater|null $updater
32     * @return bool
33     */
34    public static function onLoadExtensionSchemaUpdates( ?DatabaseUpdater $updater = null ) {
35        global $wgMathWmcServer;
36        $type = $updater->getDB()->getType();
37        if ( $type == "mysql" ) {
38            $dir = __DIR__ . '/../db/';
39            $updater->addExtensionTable( 'mathindex', $dir . 'mathindex.sql' );
40            $updater->addExtensionTable( 'mathobservation', $dir . 'mathobservation.sql' );
41            $updater->addExtensionTable( 'mathvarstat', $dir . 'mathvarstat.sql' );
42            $updater->addExtensionTable( 'mathrevisionstat', $dir . 'mathrevisionstat.sql' );
43            $updater->addExtensionTable( 'mathsemantics', $dir . 'mathsemantics.sql' );
44            $updater->addExtensionTable( 'mathperformance', $dir . 'mathperformance.sql' );
45            $updater->addExtensionTable( 'mathidentifier', $dir . 'mathidentifier.sql' );
46            $updater->addExtensionTable( 'mathlog', $dir . 'mathlog.sql' );
47            $updater->addExtensionTable( 'math_mlp', $dir . 'math_mlp.sql' );
48            $updater->addExtensionTable( 'math_review_list', "{$dir}math_review_list.sql" );
49            $updater->addExtensionTable( 'math_wbs_entity_map', "{$dir}math_wbs_entity_map.sql" );
50            $updater->addExtensionTable( 'math_wbs_text_store', "{$dir}math_wbs_text_store.sql" );
51            $updater->addExtensionTable( 'mathpagesimilarity', "{$dir}mathpagesimilarity.sql" );
52            if ( $wgMathWmcServer ) {
53                $wmcDir = $dir . 'wmc/persistent/';
54                $updater->addExtensionTable( 'math_wmc_ref', $wmcDir . "math_wmc_ref.sql" );
55                $updater->addExtensionTable( 'math_wmc_runs', $wmcDir . "math_wmc_runs.sql" );
56                $updater->addExtensionTable( 'math_wmc_results', $wmcDir . "math_wmc_results.sql" );
57                $updater->addExtensionTable( 'math_wmc_assessed_formula',
58                    $wmcDir . "math_wmc_assessed_formula.sql" );
59                $updater->addExtensionTable( 'math_wmc_assessed_revision',
60                    $wmcDir . "math_wmc_assessed_revision.sql" );
61
62            }
63        } else {
64            throw new Exception( "MathSearch extension does not currently support $type database." );
65        }
66        return true;
67    }
68
69    /**
70     * Updates the formula index in the database
71     *
72     * @param int $revId Page-ID
73     * @param string $eid Equation-ID (get updated incrementally for every math element on the page)
74     * @param MathRenderer $renderer
75     * @param ?DBConnRef $dbr
76     */
77    private static function updateIndex( int $revId, string $eid, MathRenderer $renderer,
78                                         ?DBConnRef $dbr = null ) {
79        if ( $revId > 0 && $eid ) {
80            try {
81                $inputHash = $renderer->getInputHash();
82                $tex = $renderer->getTex();
83                $mo = MathObject::cloneFromRenderer( $renderer );
84                if ( !$mo->isInDatabase() ) {
85                    $mo->writeToCache();
86                }
87                $exists = ( $dbr ?? MediaWikiServices::getInstance()->getConnectionProvider()
88                    ->getReplicaDatabase() )->selectRow( 'mathindex',
89                    [ 'mathindex_revision_id', 'mathindex_anchor', 'mathindex_inputhash' ],
90                    [
91                        'mathindex_revision_id' => $revId,
92                        'mathindex_anchor' => $eid,
93                        'mathindex_inputhash' => $inputHash
94                    ]
95                );
96                if ( $exists ) {
97                    LoggerFactory::getInstance(
98                        'MathSearch'
99                    )->warning( 'Index $' . $tex . '$ already in database.' );
100                    LoggerFactory::getInstance(
101                        'MathSearch'
102                    )->warning( "$revId-$eid with hash " . bin2hex( $inputHash ) );
103                } else {
104                        self::writeMathIndex( $revId, $eid, $inputHash, $tex );
105                }
106            } catch ( Exception $e ) {
107                LoggerFactory::getInstance( "MathSearch" )->error( 'Problem writing to math index!'
108                    . ' You might want the rebuild the index by running:'
109                    . '"php extensions/MathSearch/ReRenderMath.php". The error is'
110                    . $e->getMessage() );
111            }
112        }
113    }
114
115    /**
116     * Changes the specified defaultID given as argument ID to
117     * either the manually assignedID from the MathTag or
118     * prefixes it with "math" to increase the probability of
119     * having a unique id that can be referenced via the anchor
120     * #math{$id}.
121     * @param string &$id
122     * @param MathRenderer $renderer
123     * @param int $revId
124     * @return bool|null true if an ID has been assigned manually,
125     * false if the automatic fallback math{$id} was used.
126     */
127    public static function setMathId( &$id, MathRenderer $renderer, $revId ) {
128        if ( $revId > 0 ) {
129            if ( $renderer->getID() ) {
130                $id = $renderer->getID();
131                return true;
132            } else {
133                if ( $id === null ) {
134                    try {
135                        $id = self::getRevIdGenerator( $revId )->guessIdFromContent( $renderer->getUserInputTex() );
136                    } catch ( Exception $e ) {
137                        LoggerFactory::getInstance( "MathSearch" )->warning( "Error generating Math ID", [ $e ] );
138                        return false;
139                    }
140                    $renderer->setID( $id );
141                    return true;
142                }
143                return false;
144            }
145        }
146    }
147
148    /**
149     * Callback function that is called after a formula was rendered
150     * @param Parser $parser
151     * @param MathRenderer $renderer
152     * @param string|null &$Result reference to the rendering result
153     * @return bool
154     */
155    static function updateMathIndex( Parser $parser, MathRenderer $renderer, &$Result = null ) {
156        $revId = $parser->getRevisionId();
157        // Only store something if a pageid was set.
158        if ( $revId <= 0 ) {
159            return true;
160        }
161        // Use manually assigned IDs whenever possible
162        // and fallback to automatic IDs otherwise.
163        $hasEid = self::setMathId( $eid, $renderer, $revId );
164        if ( $eid === null ) {
165            return true;
166        }
167        if ( $hasEid === false ) {
168            $Result =
169                preg_replace( '/(class="mwe-math-mathml-(inline|display))/', "id=\"$eid\" \\1",
170                    $Result );
171        }
172        self::updateIndex( $revId, $eid, $renderer );
173
174        return true;
175    }
176
177    /**
178     * Callback function that is called after a formula was rendered
179     * @param Parser $parser
180     * @param MathRenderer $renderer
181     * @param string|null &$Result reference to the rendering result
182     * @return bool
183     */
184    static function addIdentifierDescription(
185        Parser $parser, MathRenderer $renderer, &$Result = null
186    ) {
187        $revId = $parser->getRevisionId();
188        self::setMathId( $eid, $renderer, $revId );
189        $mo = MathObject::cloneFromRenderer( $renderer );
190        $mo->setRevisionID( $revId );
191        $mo->setID( $eid );
192        $Result = preg_replace_callback( "#<(mi|mo)( ([^>].*?))?>(.*?)</\\1>#u",
193            [ $mo, 'addIdentifierTitle' ], $Result );
194        return true;
195    }
196
197    /**
198     * Callback function that is called after a formula was rendered
199     * @param Parser $parser
200     * @param MathRenderer $renderer
201     * @param string|null &$Result reference to the rendering result
202     * @return bool
203     */
204    static function addLinkToFormulaInfoPage(
205        Parser $parser, MathRenderer $renderer, &$Result = null
206    ) {
207        global $wgMathSearchInfoPage;
208        $revId = $parser->getRevisionId();
209        if ( $revId == 0 || self::setMathId( $eid, $renderer, $revId ) === false ) {
210            return true;
211        }
212        $url = SpecialPage::getTitleFor( $wgMathSearchInfoPage )->getLocalURL( [
213            'pid' => $revId,
214            'eid' => $eid
215        ] );
216        $Result = "<span><a href=\"$url\" id=\"$eid\" style=\"color:inherit;\">$Result</a></span>";
217        return true;
218    }
219
220    /**
221     * Alternative Callback function that is called after a formula was rendered
222     * used for test corpus generation for NTCIR11 Math-2
223     * You can enable this alternative hook via setting
224     * <code>$wgHooks['MathFormulaRendered'] = array(
225     *      'MathSearchHooks::onMathFormulaRenderedNoLink'
226     * );</code>
227     * in your local settings
228     *
229     * @param Parser $parser
230     * @param MathRenderer $renderer
231     * @param string|null &$Result
232     * @return bool
233     */
234    static function onMathFormulaRenderedNoLink(
235        Parser $parser, MathRenderer $renderer, &$Result = null
236    ) {
237        $revId = $parser->getRevisionId();
238        if ( !self::setMathId( $eid, $renderer, $revId ) ) {
239            return true;
240        }
241        if ( $revId > 0 ) { // Only store something if a pageid was set.
242            self::updateIndex( $revId, $eid, $renderer );
243        }
244        if ( preg_match( '#<math(.*)?\sid="(?P<id>[\w\.]+)"#', $Result, $matches ) ) {
245            $rendererId = $matches['id'];
246            $Result = str_replace( $rendererId, $eid, $Result );
247        }
248        return true;
249    }
250
251    static function generateMathAnchorString( $revId, $anchorID, $prefix = "#" ) {
252        $result = "{$prefix}math.$revId.$anchorID";
253        MediaWikiServices::getInstance()->getHookContainer()->run( "MathSearchGenerateAnchorString",
254            [ $revId, $anchorID, $prefix, &$result ] );
255        return $result;
256    }
257
258    /**
259     * @param int $oldID
260     * @param string $eid
261     * @param string $inputHash
262     * @param string $tex
263     */
264    public static function writeMathIndex( $oldID, $eid, $inputHash, $tex ) {
265        LoggerFactory::getInstance( "MathSearch" )->warning(
266            "Store index for \$$tex\$ in database with id $eid for revision $oldID." );
267        $dbw = MediaWikiServices::getInstance()
268            ->getConnectionProvider()
269            ->getPrimaryDatabase();
270        $dbw->onTransactionCommitOrIdle( static function () use ( $oldID, $eid, $inputHash, $dbw ) {
271            $dbw->replace( 'mathindex', [ [ 'mathindex_revision_id', 'mathindex_anchor' ] ], [
272                'mathindex_revision_id' => $oldID,
273                'mathindex_anchor' => $eid,
274                'mathindex_inputhash' => $inputHash
275            ] );
276        } );
277    }
278
279    /**
280     * Register the <mquery> tag with the Parser.
281     *
282     * @param Parser $parser instance of Parser
283     * @return bool true
284     */
285    static function onParserFirstCallInit( $parser ) {
286        $parser->setHook( 'mquery', [ 'MathSearchHooks', 'mQueryTagHook' ] );
287        LoggerFactory::getInstance( 'MathSearch' )->warning( 'mquery tag registered' );
288        return true;
289    }
290
291    /**
292     * Callback function for the <mquery> parser hook.
293     *
294     * @param string $content the LaTeX+MWS query input
295     * @param array $attributes
296     * @param Parser $parser
297     * @return string|string[]
298     */
299    static function mQueryTagHook( $content, $attributes, $parser ) {
300        global $wgMathDefaultLaTeXMLSetting;
301        if ( trim( $content ) === '' ) { // bug 8372
302            return '';
303        }
304        LoggerFactory::getInstance( 'MathSearch' )->debug( 'Render mquery tag.' );
305        // TODO: Report %\n problem to LaTeXML upstream
306        $content = preg_replace( '/%\n/', '', $content );
307        $renderer = new MathLaTeXML( $content );
308        $mQuerySettings = $wgMathDefaultLaTeXMLSetting;
309        $mQuerySettings['preload'][] = 'mws.sty';
310        $renderer->setLaTeXMLSettings( $mQuerySettings );
311        $renderer->render();
312        $renderedMath = $renderer->getHtmlOutput();
313        $renderer->writeCache();
314
315        return [ $renderedMath, "markerType" => 'nowiki' ];
316    }
317
318    static function onArticleDeleteComplete(
319        $article, User $user, $reason, $id, $content, $logEntry
320    ) {
321        $revId = $article->getTitle()->getLatestRevID();
322        $mathEngineBaseX = new MathEngineBaseX();
323        if ( $mathEngineBaseX->update( "", [ $revId ] ) ) {
324            LoggerFactory::getInstance( 'MathSearch' )->warning( "Deletion of $revId was successful." );
325        } else {
326            LoggerFactory::getInstance( 'MathSearch' )->warning( "Deletion of $revId failed." );
327        }
328    }
329
330    /**
331     * This occurs when an article is undeleted (restored).
332     * The formulae of the undeleted article are restored then in the index.
333     * @param Title $title Title corresponding to the article restored
334     * @param bool $create Whether the restoration caused the page to be created.
335     * @param string $comment Comment explaining the undeletion.
336     * @param int $oldPageId ID of page previously deleted. ID will be used for restored page.
337     * @param array $restoredPages Set of page IDs that have revisions restored for undelete.
338     * @return true
339     */
340    public static function onArticleUndelete(
341        Title $title, $create, $comment, $oldPageId, $restoredPages
342    ) {
343        if ( MediaWikiServices::getInstance()
344                ->getRevisionLookup()
345                ->getRevisionByPageId( $oldPageId )
346                ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
347                ->getModel() !== CONTENT_MODEL_WIKITEXT
348        ) {
349            // Skip pages that do not contain wikitext
350            return true;
351        }
352        $revId = $title->getLatestRevID();
353        $harvest = self::getMwsHarvest( $revId );
354        $mathEngineBaseX = new MathEngineBaseX();
355        if ( $mathEngineBaseX->update( $harvest, [] ) ) {
356            LoggerFactory::getInstance( 'MathSearch' )->warning( "Restoring of $revId was successful." );
357        } else {
358            LoggerFactory::getInstance( 'MathSearch' )->warning( "Restoring of $revId failed." );
359        }
360        return true;
361    }
362
363    /**
364     * Occurs after the save page request has been processed.
365     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
366     *
367     * @param WikiPage $wikiPage
368     * @param MediaWiki\User\UserIdentity $user
369     * @param string $summary
370     * @param int $flags
371     * @param MediaWiki\Revision\RevisionRecord $revisionRecord
372     *
373     * @return bool
374     */
375    public static function onPageSaveComplete(
376        WikiPage $wikiPage, $user, $summary, $flags, $revisionRecord
377    ) {
378        if ( $revisionRecord
379            ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
380            ->getModel() !== CONTENT_MODEL_WIKITEXT
381        ) {
382            // Skip pages that do not contain wikitext
383            return true;
384        }
385
386        $revId = $revisionRecord->getId();
387        $harvest = self::getMwsHarvest( $revId );
388        $previousRevisionRecord = MediaWikiServices::getInstance()
389            ->getRevisionLookup()
390            ->getPreviousRevision( $revisionRecord );
391        $res = false;
392        $baseXUpdater = new MathEngineBaseX();
393        try {
394            if ( $previousRevisionRecord != null ) {
395                $prevRevId = $previousRevisionRecord->getId();
396                # delete the entries previous revision.
397                $res = $baseXUpdater->update( $harvest, [ $prevRevId ] );
398            } else {
399                # just create a new entry in index.
400                $res = $baseXUpdater->update( $harvest, [] );
401                $prevRevId = -1;
402            }
403        } catch ( Exception $e ) {
404            LoggerFactory::getInstance( 'MathSearch' )
405                ->warning( 'Harvest update failed: {exception}',
406                    [ 'exception' => $e->getMessage() ] );
407        }
408        if ( $res ) {
409            LoggerFactory::getInstance(
410                'MathSearch'
411            )->warning( "Update for $revId (was $prevRevId) successful." );
412        } else {
413            LoggerFactory::getInstance(
414                'MathSearch'
415            )->warning( "Update for $revId (was $prevRevId) failed." );
416        }
417    }
418
419    /**
420     * Occurs after the save page request has been processed.
421     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete
422     * @deprecated
423     * PageContentSaveComplete: legacy hook, deprecated in favor of PageSaveComplete
424     *
425     * @param WikiPage $wikiPage
426     * @param User $user
427     * @param Content $content
428     * @param string $summary
429     * @param bool $isMinor
430     * @param bool $isWatch
431     * @param string $section Deprecated
432     * @param int $flags
433     * @param Revision|null $revision
434     * @param Status $status
435     * @param int $baseRevId
436     *
437     * @return bool
438     */
439    public static function onPageContentSaveComplete(
440        WikiPage $wikiPage, $user, $content, $summary, $isMinor,
441        $isWatch, $section, $flags, $revision, $status, $baseRevId
442    ) {
443        // TODO: Update to JOB
444        if ( $revision == null ) {
445            LoggerFactory::getInstance(
446                'MathSearch'
447            )->warning( "Empty update for {$wikiPage->getTitle()->getFullText()}." );
448            return true;
449        }
450        $revisionRecord = $revision->getRevisionRecord();
451        self::onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord );
452
453        return true;
454    }
455
456    /**
457     * Enable latexml rendering mode as option by default
458     */
459    public static function registerExtension() {
460        global $wgMathValidModes;
461        if ( !in_array( 'latexml', $wgMathValidModes ) ) {
462            $wgMathValidModes[] = 'latexml';
463        }
464    }
465
466    /**
467     * @param int $revId
468     * @return MathIdGenerator
469     */
470    private static function getRevIdGenerator( $revId ) {
471        self::$idGenerators[$revId] ??= MathIdGenerator::newFromRevisionId( $revId );
472        return self::$idGenerators[$revId];
473    }
474
475    /**
476     * @param int|null $revId
477     * @return string
478     */
479    protected static function getMwsHarvest( ?int $revId ): string {
480        $idGenerator = MathIdGenerator::newFromRevisionId( $revId );
481        $mathTags = $idGenerator->getMathTags();
482        $harvest = "";
483        try {
484            if ( $mathTags ) {
485                $dw = new MwsDumpWriter();
486                foreach ( $mathTags as $tag ) {
487                    $id = null;
488                    $tagContent = $tag[MathIdGenerator::CONTENT_POS];
489                    $attributes = $tag[MathIdGenerator::ATTRIB_POS];
490                    $renderer = MathRenderer::getRenderer( $tagContent, $attributes, 'latexml' );
491                    $renderer->render();
492                    self::setMathId( $id, $renderer, $revId );
493                    $dw->addMwsExpression( $renderer->getMathml(), $revId, $id );
494                }
495                $harvest = $dw->getOutput();
496            }
497        } catch ( Exception $e ) {
498            LoggerFactory::getInstance( 'MathSearch' )
499                ->warning( 'Harvest strinc can not be generated: {exception}',
500                    [ 'exception' => $e->getMessage() ] );
501        }
502        return $harvest;
503    }
504}