Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 253
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMathSearch
0.00% covered (danger)
0.00%
0 / 253
0.00% covered (danger)
0.00%
0 / 19
4290
0.00% covered (danger)
0.00%
0 / 1
 exception_error_handler
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __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 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 searchForm
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchRows
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 processInput
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 performSearch
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
132
 displayMathElements
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
90
 getElementById
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 printTerm
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 highlightHit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 printSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 displayRevisionResults
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 render
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addTerm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableMathStyles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addFormData
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDefault
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Extension\Math\MathLaTeXML;
4use MediaWiki\Extension\Math\MathMathML;
5use MediaWiki\Logger\LoggerFactory;
6use MediaWiki\MediaWikiServices;
7
8/**
9 * MediaWiki MathSearch extension
10 *
11 * (c) 2012 Moritz Schubotz
12 * GPLv2 license; info in main package.
13 *
14 * @file
15 * @ingroup extensions
16 */
17class SpecialMathSearch extends SpecialPage {
18
19    private const GUI_PATH = '/modules/min/index.xhtml';
20
21    private $mathpattern;
22    private $textpattern;
23    private $mathmlquery;
24    /** @var string */
25    private $mathEngine = 'mws';
26    private $displayQuery;
27    private $mathBackend;
28    private $resultID = 0;
29    private $noTerms = 1;
30    private $terms = [];
31    /** @var int[] */
32    private $relevanceMap;
33    private $defaults;
34
35    public static function exception_error_handler( $errno, $errstr, $errfile, $errline ) {
36        if ( !( error_reporting() & $errno ) ) {
37            // This error code is not included in error_reporting
38            return;
39        }
40        throw new ErrorException( $errstr, 0, $errno, $errfile, $errline );
41    }
42
43    function __construct() {
44        parent::__construct( 'MathSearch' );
45    }
46
47    /**
48     * The main function
49     * @param string|null $par
50     */
51    public function execute( $par ) {
52        set_error_handler( "SpecialMathSearch::exception_error_handler" );
53        $request = $this->getRequest();
54        $this->setHeaders();
55        $this->mathpattern = $request->getText( 'mathpattern', '' );
56        $this->textpattern = $request->getText( 'textpattern', '' );
57        $this->noTerms = $request->getText( 'wpnoTerms', $this->noTerms );
58        $isEncoded = $request->getBool( 'isEncoded', false );
59        if ( $isEncoded ) {
60            $this->mathpattern = htmlspecialchars_decode( $this->mathpattern );
61        }
62        if ( $this->mathpattern || $this->textpattern ) {
63            $i = 0;
64            if ( $this->mathpattern ) {
65                $i++;
66                $this->addFormData( $this->mathpattern, $i,
67                    MathSearchTerm::TYPE_MATH, MathSearchTerm::REL_AND );
68            }
69            if ( $this->textpattern ) {
70                $i++;
71                $this->addFormData( $this->textpattern, $i,
72                    MathSearchTerm::TYPE_TEXT, MathSearchTerm::REL_AND );
73            }
74            $this->noTerms = $i;
75            $form = $this->searchForm();
76            $form->prepareForm();
77            $res = $form->trySubmit();
78            $this->getOutput()->addHTML( $form->getHTML( $res ) );
79            // $this->performSearch();
80            // $this->performSearch();
81        } else {
82            $this->searchForm()->show();
83            if ( file_exists( __DIR__ . self::GUI_PATH ) ) {
84                $minurl = $this->getConfig()->get( 'ExtensionAssetsPath' ) . '/MathSearch' . self::GUI_PATH;
85                $this->getOutput()
86                    ->addHTML( "<p><a href=\"{$minurl}\">Test experimental math input interface</a></p>" );
87            }
88        }
89        restore_error_handler();
90    }
91
92    /**
93     * Generates the search input form
94     *
95     * @return HTMLForm
96     */
97    private function searchForm() {
98        # A formDescriptor Array to tell HTMLForm what to build
99        $formDescriptor = [
100            'mathEngine' => [
101                'label' => 'Math engine',
102                'class' => 'HTMLSelectField',
103                'options' => [
104                    'MathWebSearch' => 'mws',
105                    'BaseX' => 'basex'
106                ],
107                'default' => $this->mathEngine,
108            ],
109            'displayQuery' => [
110                'label' => 'Display search query',
111                'type' => 'check',
112                'default' => $this->displayQuery,
113            ],
114            'noTerms' => [
115                'label' => 'Number of search terms',
116                'type' => 'int',
117                'min' => 1,
118                'default' => $this->noTerms,
119            ],
120        ];
121        $formDescriptor = array_merge( $formDescriptor, $this->getSearchRows( $this->noTerms ) );
122        $htmlForm =    new HTMLForm( $formDescriptor, $this->getContext() );
123        $htmlForm->setSubmitText( 'Search' );
124        $htmlForm->setSubmitCallback( [ $this, 'processInput' ] );
125        $htmlForm->setHeaderText( "<h2>Input</h2>" );
126        // $htmlForm->show();
127        return $htmlForm;
128    }
129
130    private function getSearchRows( $cnt ) {
131        $out = [];
132        for ( $i = 1; $i <= $cnt; $i++ ) {
133            if ( $i == 1 ) {
134                // Hide the meaningless first relation from the user
135                $relType = 'hidden';
136            } else {
137                $relType = 'select';
138            }
139            $out["rel-$i"] = [
140                'label-message' => 'math-search-relation-label',
141                'options' => [
142                    $this->msg( 'math-search-relation-0' )->text() => 0,
143                    $this->msg( 'math-search-relation-1' )->text() => 1,
144                    $this->msg( 'math-search-relation-2' )->text() => 2 // ,
145                    // 'nor' => 3
146                ],
147                'type' => $relType,
148                'default' => $this->getDefault( $i, 'rel' ),
149                'section' => "term $i" // TODO: figure out how to localize section with parameter
150            ];
151            $out["type-$i"] = [
152                'label-message' => 'math-search-type-label',
153                'options' => [
154                    $this->msg( 'math-search-type-0' )->text() => 0,
155                    $this->msg( 'math-search-type-1' )->text() => 1,
156                    $this->msg( 'math-search-type-2' )->text() => 2
157                ],
158                'type' => 'select',
159                'section' => "term $i",
160                'default' => $this->getDefault( $i, 'type' )
161            ];
162            $out["expr-$i"] = [
163                'label-message' => 'math-search-expression-label',
164                'type' => 'text',
165                'section' => "term $i",
166                'default' => $this->getDefault( $i, 'expr' )
167            ];
168        }
169        return $out;
170    }
171
172    /**
173     * Processes the submitted Form input
174     * @param array $formData
175     * @return bool
176     */
177    public function processInput( $formData ) {
178        if ( $formData['noTerms'] != $this->noTerms ) {
179            $this->noTerms = $formData['noTerms'];
180            $this->searchForm();
181            return true;
182        }
183
184        for ( $i = 1; $i <= $this->noTerms; $i++ ) {
185            $this->addTerm( $i, $formData["rel-$i"], $formData["type-$i"],
186                $formData["expr-$i"] );
187        }
188
189        $this->mathEngine = $formData['mathEngine'];
190        $this->displayQuery = $formData['displayQuery'];
191        $this->performSearch();
192    }
193
194    public function performSearch() {
195        $out = $this->getOutput();
196        $out->addWikiTextAsInterface( '==Results==' );
197        $out->addWikiTextAsInterface( 'You searched for the following terms:' );
198        switch ( $this->mathEngine ) {
199            case 'basex':
200                $this->mathBackend = new MathEngineBaseX( null );
201                break;
202            default:
203                $this->mathBackend = new MathEngineMws( null );
204        }
205        /** @var MathSearchTerm $term */
206        foreach ( $this->terms as $term ) {
207            if ( $term->getExpr() == "" ) {
208                continue;
209            }
210            $term->doSearch( $this->mathBackend );
211            $this->enableMathStyles();
212            $this->printTerm( $term );
213            if ( $term->getKey() == 1 ) {
214                $this->relevanceMap = $term->getRelevanceMap();
215
216            } else {
217                switch ( $term->getRel() ) {
218                    case $term::REL_AND:
219                        $this->relevanceMap =
220                            array_intersect( $this->relevanceMap, $term->getRelevanceMap() );
221                        break;
222                    case $term::REL_OR:
223                        $this->relevanceMap = $this->relevanceMap + $term->getRelevanceMap();
224                        break;
225                    case $term::REL_NAND:
226                        $this->relevanceMap =
227                            array_diff( $this->relevanceMap, $term->getRelevanceMap() );
228                    // case $term::REL_NOR: (too many results)
229                }
230            }
231        }
232        $formulaCount = 0;
233        if ( $this->relevanceMap != null ) {
234            $formulaCount = count( $this->relevanceMap, true );
235            foreach ( $this->relevanceMap as $revisionID ) {
236                $this->displayRevisionResults( $revisionID );
237            }
238        }
239        $this->getOutput()->addWikiTextAsInterface( "In total " . $formulaCount . ' results.' );
240    }
241
242    /**
243     * @param int $revisionID
244     * @param array[] $mathElements
245     * @param string $pagename
246     */
247    public function displayMathElements( $revisionID, $mathElements, $pagename ) {
248        $out = $this->getOutput();
249        foreach ( $mathElements as $anchorID => $answ ) {
250            $res = MathObject::constructformpage( $revisionID, $anchorID );
251            if ( !$res ) {
252                LoggerFactory::getInstance(
253                    'MathSearch'
254                )->error( "Failure: Could not get entry $anchorID for page $pagename (id $revisionID)" );
255                return;
256            }
257            $mml = $res->getMathml();
258            if ( !$mml ) {
259                LoggerFactory::getInstance( 'MathSearch' )
260                    ->error( "Failure: Could not get MathML $anchorID for page $pagename (id $revisionID)" );
261                continue;
262            }
263            $out->addWikiTextAsInterface( "====[[$pagename#$anchorID|Eq: $anchorID (Result " .
264                $this->resultID++ . ")]]====", false );
265            $out->addHTML( "<br />" );
266            $xpath = $answ[0]['xpath'];
267            // TODO: Remove hack and report to Prode that he fixes that
268            // $xmml->registerXPathNamespace('m', 'http://www.w3.org/1998/Math/MathML');
269            $xpath =
270                str_replace( '/m:semantics/m:annotation-xml[@encoding="MathML-Content"]',
271                    '', $xpath );
272            $dom = new DOMDocument;
273            $dom->preserveWhiteSpace = false;
274            $dom->validateOnParse = true;
275            $dom->loadXML( $mml );
276            $DOMx = new DOMXpath( $dom );
277            $hits = $DOMx->query( $xpath );
278            if ( $this->getConfig()->get( 'MathDebug' ) ) {
279                LoggerFactory::getInstance( 'MathSearch' )->debug( 'xPATH:' . $xpath );
280            }
281            if ( $hits !== null && $hits ) {
282                foreach ( $hits as $node ) {
283                    $this->highlightHit( $node, $dom, $mml );
284                }
285            }
286            if ( $mml != $res->getMathml() ) {
287                $renderer = new MathMathML( $mml, [ 'type' => 'pmml' ] );
288                $renderer->setMathml( $mml );
289                $renderer->render();
290                $out->addHTML( $renderer->getHtmlOutput() );
291                $renderer->writeCache();
292            } else {
293                $res->render();
294                $out->addHTML( $res->getHtmlOutput() );
295            }
296
297        }
298    }
299
300    /**
301     * Note that the default getElementById function
302     * <code>
303     *  $dom->getElementById( $id );
304     * </code>
305     * works for "xml:id" only,  but not for "id" which is extended to "math:id"
306     *     TODO: could be fixed with
307     * @link http://php.net/manual/de/domdocument.getelementbyid.php#86056
308     * @param string $id
309     * @param DOMDocument $doc
310     * @return DOMElement|null
311     */
312    private function getElementById( $id, $doc ) {
313        $xpath = new DOMXPath( $doc );
314        return $xpath->query( "//*[@id='$id']" )->item( 0 );
315    }
316
317    /**
318     * @param MathSearchTerm $term
319     */
320    public function printTerm( $term ) {
321        if ( $term->getType() == MathSearchTerm::TYPE_MATH ) {
322            $expr = "<math>{$term->getExpr()}</math>";
323        } else {
324            $expr = "<code>{$term->getExpr()}</code>";
325        }
326        $this->getOutput()->addWikiMsg( 'math-search-term',
327            $term->getKey(),
328            $expr,
329            $this->msg( "math-search-type-{$term->getType()}" )->text(),
330            $term->getRel() == '' ? '' : $this->msg( "math-search-relation-{$term->getRel()}" )->text(),
331            count( $term->getRelevanceMap() ) );
332    }
333
334    /**
335     * @param DOMNode $node
336     * @param DOMDocument $dom
337     * @param string &$mml
338     */
339    protected function highlightHit( $node, $dom, &$mml ) {
340        if ( $node == null || !$node->hasAttributes() ) {
341            return;
342        }
343        try {
344            $xRef = $node->attributes->getNamedItem( 'xref' );
345            if ( $xRef ) {
346                $domRes = $this->getElementById( $xRef->nodeValue, $dom );
347                if ( $domRes ) {
348                    $domRes->setAttribute( 'mathcolor', '#cc0000' );
349                    $mml = $domRes->ownerDocument->saveXML();
350                }
351            } else {
352                // CMML node has no corresponding PMML element
353                $fallback = $node->parentNode;
354                $this->highlightHit( $fallback, $dom, $mml );
355            }
356        } catch ( Exception $e ) {
357            LoggerFactory::getInstance(
358                'MathSearch'
359            )->error( 'Problem highlighting hit ' . $e->getMessage() );
360        }
361    }
362
363    /**
364     * @param string $src
365     * @param string $lang the language of the source snippet
366     */
367    private function printSource( $src, $lang = "xml" ) {
368        $out = $this->getOutput();
369        $out->addWikiTextAsInterface( '<source lang="' . $lang . '">' . $src . '</source>' );
370    }
371
372    /**
373     * Displays the equations for one page
374     *
375     * @param int $revisionID
376     *
377     * @return bool
378     */
379    function displayRevisionResults( $revisionID ) {
380        $out = $this->getOutput();
381        $revisionStoreRecord = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionById( $revisionID );
382        if ( !$revisionStoreRecord ) {
383            LoggerFactory::getInstance( 'MathSearch' )->error( 'invalid revision number' );
384            return false;
385        }
386        $title = $revisionStoreRecord->getPageAsLinkTarget(); # MCR migration note: this replaced Revision::getTitle
387        $pagename = (string)$title;
388        $mathElements = [];
389        $textElements = [];
390        /** @var MathSearchTerm $term */
391        foreach ( $this->terms as $term ) {
392            if ( $term->getExpr() == "" ) {
393                continue;
394            }
395            if ( $term->getType() == MathSearchTerm::TYPE_MATH ) {
396                $mathElements += $term->getRevisionResult( $revisionID );
397            } elseif ( $term->getType() == MathSearchTerm::TYPE_TEXT ) { // Forward compatible
398                /** @var SearchResult $textResult */
399                $textResult = $term->getRevisionResult( $revisionID );
400                // see: T90976
401                $textElements[] = $textResult->getTextSnippet( [ $term->getExpr() ] );
402                // $textElements[]=$textResult->getSectionSnippet();
403            }
404        }
405        $out->addWikiTextAsInterface( "=== [[Special:Permalink/$revisionID | $pagename]] ===" );
406        foreach ( $textElements as $textResult ) {
407            $out->addWikiTextAsInterface( $textResult );
408        }
409        LoggerFactory::getInstance( 'MathSearch' )->warning( "Processing results for $pagename" );
410        $this->displayMathElements( $revisionID, $mathElements, $pagename );
411        return true;
412    }
413
414    /**
415     * Renders the math search input to mathml
416     * @return bool
417     */
418    function render() {
419        $renderer = new MathLaTeXML( $this->mathpattern );
420        $renderer->setLaTeXMLSettings( 'profile=mwsquery' );
421        $renderer->setAllowedRootElements( [ 'query' ] );
422        $renderer->render( true );
423        $this->mathmlquery = $renderer->getMathml();
424
425        return $this->mathmlquery !== '';
426    }
427
428    /**
429     * @param int $i
430     * @param int $rel
431     * @param int $type
432     * @param string $expr
433     */
434    private function addTerm( $i, $rel, $type, $expr ) {
435        $this->terms[ $i ] = new MathSearchTerm( $i, $rel, $type, $expr );
436    }
437
438    private function enableMathStyles() {
439        $out = $this->getOutput();
440        $out->addModuleStyles(
441            [ 'ext.math.styles', 'ext.math.desktop.styles', 'ext.math.scripts' ]
442        );
443    }
444
445    private function addFormData( $mathpattern, $i, $TYPE_MATH, $REL_AND ) {
446        $this->defaults[$i]['type'] = $TYPE_MATH;
447        $this->defaults[$i]['rel'] = $REL_AND;
448        $this->defaults[$i]['expr'] = $mathpattern;
449    }
450
451    private function getDefault( $i, $what ) {
452        if ( isset( $this->defaults[$i][$what] ) ) {
453            return $this->defaults[$i][$what];
454        } else {
455            switch ( $what ) {
456                case 'expr':
457                    return '';
458                case 'type':
459                    return MathSearchTerm::TYPE_MATH;
460                case 'rel':
461                    return MathSearchTerm::REL_AND;
462            }
463            return "";
464        }
465    }
466
467    protected function getGroupName() {
468        return 'mathsearch';
469    }
470}