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