Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.12% |
2 / 64 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
Search | |
3.12% |
2 / 64 |
|
0.00% |
0 / 8 |
347.20 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
doSearchQuery | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
categoryCondition | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
prefixCondition | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
regexCond | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMatchingTitles | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
getReplacedText | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
getReplacedTitle | |
0.00% |
0 / 3 |
|
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 | */ |
20 | namespace MediaWiki\Extension\ReplaceText; |
21 | |
22 | use MediaWiki\Config\Config; |
23 | use MediaWiki\Title\Title; |
24 | use Wikimedia\Rdbms\IConnectionProvider; |
25 | use Wikimedia\Rdbms\IExpression; |
26 | use Wikimedia\Rdbms\IReadableDatabase; |
27 | use Wikimedia\Rdbms\IResultWrapper; |
28 | use Wikimedia\Rdbms\LikeValue; |
29 | use Wikimedia\Rdbms\SelectQueryBuilder; |
30 | |
31 | class 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 | } |