Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.12% covered (danger)
3.12%
2 / 64
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Search
3.12% covered (danger)
3.12%
2 / 64
0.00% covered (danger)
0.00%
0 / 8
347.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 doSearchQuery
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 categoryCondition
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 prefixCondition
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 regexCond
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getMatchingTitles
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getReplacedText
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 getReplacedTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * https://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\Extension\ReplaceText;
21
22use MediaWiki\Config\Config;
23use MediaWiki\Title\Title;
24use Wikimedia\Rdbms\IConnectionProvider;
25use Wikimedia\Rdbms\IExpression;
26use Wikimedia\Rdbms\IReadableDatabase;
27use Wikimedia\Rdbms\IResultWrapper;
28use Wikimedia\Rdbms\LikeValue;
29use Wikimedia\Rdbms\SelectQueryBuilder;
30
31class Search {
32    private Config $config;
33    private IConnectionProvider $loadBalancer;
34
35    public function __construct(
36        Config $config,
37        IConnectionProvider $loadBalancer
38    ) {
39        $this->config = $config;
40        $this->loadBalancer = $loadBalancer;
41    }
42
43    /**
44     * @param string $search
45     * @param array $namespaces
46     * @param string|null $category
47     * @param string|null $prefix
48     * @param int|null $pageLimit
49     * @param bool $use_regex
50     * @return IResultWrapper Resulting rows
51     */
52    public function doSearchQuery(
53        $search, $namespaces, $category, $prefix, $pageLimit, $use_regex = false
54    ) {
55        $dbr = $this->loadBalancer->getReplicaDatabase();
56        $queryBuilder = $dbr->newSelectQueryBuilder()
57            ->select( [ 'page_id', 'page_namespace', 'page_title', 'old_text', 'slot_role_id' ] )
58            ->from( 'page' )
59            ->join( 'revision', null, 'rev_id = page_latest' )
60            ->join( 'slots', null, 'rev_id = slot_revision_id' )
61            ->join( 'content', null, 'slot_content_id = content_id' )
62            ->join( 'text', null, $dbr->buildIntegerCast( 'SUBSTR(content_address, 4)' ) . ' = old_id' );
63        if ( $use_regex ) {
64            $queryBuilder->where( self::regexCond( $dbr, 'old_text', $search ) );
65        } else {
66            $any = $dbr->anyString();
67            $queryBuilder->where( $dbr->expr( 'old_text', IExpression::LIKE, new LikeValue( $any, $search, $any ) ) );
68        }
69        $queryBuilder->andWhere( [ 'page_namespace' => $namespaces ] );
70        if ( $pageLimit === null || $pageLimit === '' ) {
71            $pageLimit = $this->config->get( 'ReplaceTextResultsLimit' );
72        }
73        self::categoryCondition( $category, $queryBuilder );
74        $this->prefixCondition( $prefix, $dbr, $queryBuilder );
75        return $queryBuilder->orderBy( [ 'page_namespace', 'page_title' ] )
76            ->limit( $pageLimit )
77            ->caller( __METHOD__ )
78            ->fetchResultSet();
79    }
80
81    /**
82     * @param string|null $category
83     * @param SelectQueryBuilder $queryBuilder
84     */
85    public static function categoryCondition( $category, SelectQueryBuilder $queryBuilder ) {
86        if ( strval( $category ) !== '' ) {
87            $category = Title::newFromText( $category )->getDbKey();
88            $queryBuilder->join( 'categorylinks', null, 'page_id = cl_from' )
89                ->where( [ 'cl_to' => $category ] );
90        }
91    }
92
93    /**
94     * @param string|null $prefix
95     * @param IReadableDatabase $dbr
96     * @param SelectQueryBuilder $queryBuilder
97     */
98    private function prefixCondition( $prefix, IReadableDatabase $dbr, SelectQueryBuilder $queryBuilder ) {
99        if ( strval( $prefix ) === '' ) {
100            return;
101        }
102
103        $title = Title::newFromText( $prefix );
104        if ( $title !== null ) {
105            $prefix = $title->getDbKey();
106        }
107        $any = $dbr->anyString();
108        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable $prefix is checked for null
109        $queryBuilder->where( $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $prefix, $any ) ) );
110    }
111
112    /**
113     * @param IReadableDatabase $dbr
114     * @param string $column
115     * @param string $regex
116     * @return string query condition for regex
117     */
118    public static function regexCond( $dbr, $column, $regex ) {
119        if ( $dbr->getType() == 'postgres' ) {
120            $cond = "$column ~ ";
121        } else {
122            $cond = "CAST($column AS BINARY) REGEXP BINARY ";
123        }
124        $cond .= $dbr->addQuotes( $regex );
125        return $cond;
126    }
127
128    /**
129     * @param string $str
130     * @param array $namespaces
131     * @param string|null $category
132     * @param string|null $prefix
133     * @param int|null $pageLimit
134     * @param bool $use_regex
135     * @return IResultWrapper Resulting rows
136     */
137    public function getMatchingTitles(
138        $str,
139        $namespaces,
140        $category,
141        $prefix,
142        $pageLimit,
143        $use_regex = false
144    ) {
145        $dbr = $this->loadBalancer->getReplicaDatabase();
146        $queryBuilder = $dbr->newSelectQueryBuilder()
147            ->select( [ 'page_title', 'page_namespace' ] )
148            ->from( 'page' );
149        $str = str_replace( ' ', '_', $str );
150        if ( $use_regex ) {
151            $queryBuilder->where( self::regexCond( $dbr, 'page_title', $str ) );
152        } else {
153            $any = $dbr->anyString();
154            $queryBuilder->where( $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $any, $str, $any ) ) );
155        }
156        $queryBuilder->andWhere( [ 'page_namespace' => $namespaces ] );
157        if ( $pageLimit === null || $pageLimit === '' ) {
158            $pageLimit = $this->config->get( 'ReplaceTextResultsLimit' );
159        }
160        self::categoryCondition( $category, $queryBuilder );
161        $this->prefixCondition( $prefix, $dbr, $queryBuilder );
162        return $queryBuilder->orderBy( [ 'page_namespace', 'page_title' ] )
163            ->limit( $pageLimit )
164            ->caller( __METHOD__ )
165            ->fetchResultSet();
166    }
167
168    /**
169     * Do a replacement on a string.
170     * @param string $text
171     * @param string $search
172     * @param string $replacement
173     * @param bool $regex
174     * @return string
175     */
176    public static function getReplacedText( $text, $search, $replacement, $regex ) {
177        if ( $regex ) {
178            $escapedSearch = addcslashes( $search, '/' );
179            return preg_replace( "/$escapedSearch/Uu", $replacement, $text );
180        } else {
181            return str_replace( $search, $replacement, $text );
182        }
183    }
184
185    /**
186     * Do a replacement on a title.
187     * @param Title $title
188     * @param string $search
189     * @param string $replacement
190     * @param bool $regex
191     * @return Title|null
192     */
193    public static function getReplacedTitle( Title $title, $search, $replacement, $regex ) {
194        $oldTitleText = $title->getText();
195        $newTitleText = self::getReplacedText( $oldTitleText, $search, $replacement, $regex );
196        return Title::makeTitleSafe( $title->getNamespace(), $newTitleText );
197    }
198}