Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.76% covered (danger)
11.76%
8 / 68
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoSearchMySQL
11.76% covered (danger)
11.76%
8 / 68
40.00% covered (danger)
40.00%
2 / 5
354.49
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchTerms
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 parseQuery
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
210
 regexTerm
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getIndexField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * We need to create subclasses, instead of just calling the functionality,
7 * because both filter() and, more importantly, $searchTerms are currently
8 * "protected".
9 *
10 * Unfortunately, the SearchMySQL methods parseQuery(), regexTerm() and
11 * getIndexField() are private, which means that they need to be
12 * copied over here (but declared as public).
13 */
14class CargoSearchMySQL extends SearchMySQL {
15
16    public function __construct() {
17        if ( property_exists( $this, 'dbProvider' ) ) {
18            // MW 1.41+
19            $dbProvider = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
20            parent::__construct( $dbProvider );
21        } else {
22            // MW < 1.41
23            $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
24            parent::__construct( $lb );
25        }
26    }
27
28    public function getSearchTerms( $searchString ) {
29        $filteredTerm = $this->filter( $searchString );
30        $this->parseQuery( $filteredTerm, false );
31        return $this->searchTerms;
32    }
33
34    /**
35     * Parse the user's query and transform it into two SQL fragments:
36     * a WHERE condition and an ORDER BY expression
37     *
38     * @param string $filteredText
39     * @param string $fulltext
40     *
41     * @return array
42     */
43    public function parseQuery( $filteredText, $fulltext ) {
44        $lc = $this->legalSearchChars( self::CHARS_NO_SYNTAX ); // Minus syntax chars (" and *)
45        $searchon = '';
46        $this->searchTerms = [];
47
48        # @todo FIXME: This doesn't handle parenthetical expressions.
49        $m = [];
50        if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
51                $filteredText, $m, PREG_SET_ORDER ) ) {
52            $contLang = CargoUtils::getContentLang();
53            $langConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
54                ->getLanguageConverter( $contLang );
55            foreach ( $m as $bits ) {
56                Wikimedia\suppressWarnings();
57                [ /* all */, $modifier, $term, $nonQuoted, $wildcard ] = $bits;
58                Wikimedia\restoreWarnings();
59
60                if ( $nonQuoted != '' ) {
61                    $term = $nonQuoted;
62                    $quote = '';
63                } else {
64                    $term = str_replace( '"', '', $term );
65                    $quote = '"';
66                }
67
68                if ( $searchon !== '' ) {
69                    $searchon .= ' ';
70                }
71                if ( $this->strictMatching && ( $modifier == '' ) ) {
72                    // If we leave this out, boolean op defaults to OR which is rarely helpful.
73                    $modifier = '+';
74                }
75
76                // Some languages such as Serbian store the input form in the search index,
77                // so we may need to search for matches in multiple writing system variants.
78                $convertedVariants = $langConverter->autoConvertToAllVariants( $term );
79                if ( is_array( $convertedVariants ) ) {
80                    $variants = array_unique( array_values( $convertedVariants ) );
81                } else {
82                    $variants = [ $term ];
83                }
84
85                // The low-level search index does some processing on input to work
86                // around problems with minimum lengths and encoding in MySQL's
87                // fulltext engine.
88                // For Chinese this also inserts spaces between adjacent Han characters.
89                $strippedVariants = array_map( [ $contLang, 'normalizeForSearch' ], $variants );
90
91                // Some languages such as Chinese force all variants to a canonical
92                // form when stripping to the low-level search index, so to be sure
93                // let's check our variants list for unique items after stripping.
94                $strippedVariants = array_unique( $strippedVariants );
95
96                $searchon .= $modifier;
97                if ( count( $strippedVariants ) > 1 ) {
98                    $searchon .= '(';
99                }
100                foreach ( $strippedVariants as $stripped ) {
101                    $stripped = $this->normalizeText( $stripped );
102                    if ( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
103                        // Hack for Chinese: we need to toss in quotes for
104                        // multiple-character phrases since normalizeForSearch()
105                        // added spaces between them to make word breaks.
106                        $stripped = '"' . trim( $stripped ) . '"';
107                    }
108                    $searchon .= "$quote$stripped$quote$wildcard ";
109                }
110                if ( count( $strippedVariants ) > 1 ) {
111                    $searchon .= ')';
112                }
113
114                // Match individual terms or quoted phrase in result highlighting...
115                // Note that variants will be introduced in a later stage for highlighting!
116                $regexp = $this->regexTerm( $term, $wildcard );
117                $this->searchTerms[] = $regexp;
118            }
119            wfDebug( __METHOD__ . ": Would search with '$searchon'\n" );
120            wfDebug( __METHOD__ . ': Match with /' . implode( '|', $this->searchTerms ) . "/\n" );
121        } else {
122            wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
123        }
124
125        if ( property_exists( $this, 'db' ) ) {
126            // MW < 1.41
127            // @phan-suppress-next-line PhanUndeclaredProperty
128            $searchon = $this->db->addQuotes( $searchon );
129        } else {
130            $cdb = CargoUtils::getDB();
131            $searchon = $cdb->addQuotes( $searchon );
132        }
133
134        $field = $this->getIndexField( $fulltext );
135        return [
136            " MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ",
137            " MATCH($field) AGAINST($searchon IN NATURAL LANGUAGE MODE) DESC "
138        ];
139    }
140
141    /**
142     * @param string $string
143     * @param bool $wildcard
144     * @return string
145     */
146    public function regexTerm( $string, $wildcard ) {
147        $regex = preg_quote( $string, '/' );
148        $contLang = CargoUtils::getContentLang();
149        if ( $contLang->hasWordBreaks() ) {
150            if ( $wildcard ) {
151                // Don't cut off the final bit!
152                $regex = "\b$regex";
153            } else {
154                $regex = "\b$regex\b";
155            }
156        } else {
157            // For Chinese, words may legitimately abut other words in the text literal.
158            // Don't add \b boundary checks... note this could cause false positives
159            // for Latin chars.
160        }
161        return $regex;
162    }
163
164    /**
165     * Picks which field to index on, depending on what type of query.
166     * @param bool $fulltext
167     * @return string
168     */
169    public function getIndexField( $fulltext ) {
170        return $fulltext ? 'si_text' : 'si_title';
171    }
172
173}