Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 163 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
AllMessagesTablePager | |
0.00% |
0 / 162 |
|
0.00% |
0 / 15 |
2652 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
getAllMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getCustomisedStatuses | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
132 | |||
reallyDoQuery | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
156 | |||
getStartBody | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 | |||
getEndBody | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatValue | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
formatRow | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getRowAttrs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCellAttrs | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getFieldNames | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFieldSortable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultSort | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 1 |
|
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 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup Pager |
20 | */ |
21 | |
22 | namespace MediaWiki\Pager; |
23 | |
24 | use LocalisationCache; |
25 | use MediaWiki\Context\IContextSource; |
26 | use MediaWiki\Html\FormOptions; |
27 | use MediaWiki\Html\Html; |
28 | use MediaWiki\Language\Language; |
29 | use MediaWiki\Languages\LanguageFactory; |
30 | use MediaWiki\Linker\LinkRenderer; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Parser\Sanitizer; |
33 | use MediaWiki\SpecialPage\SpecialPage; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\Xml\Xml; |
36 | use stdClass; |
37 | use Wikimedia\Rdbms\FakeResultWrapper; |
38 | use Wikimedia\Rdbms\IConnectionProvider; |
39 | use Wikimedia\Rdbms\IReadableDatabase; |
40 | use Wikimedia\Rdbms\IResultWrapper; |
41 | |
42 | /** |
43 | * Use TablePager for prettified output. We have to pretend that we're |
44 | * getting data from a table when in fact not all of it comes from the database. |
45 | * |
46 | * @ingroup Pager |
47 | */ |
48 | class AllMessagesTablePager extends TablePager { |
49 | |
50 | /** |
51 | * @var bool |
52 | */ |
53 | protected $foreign; |
54 | |
55 | /** |
56 | * @var string|false |
57 | */ |
58 | protected $prefix; |
59 | |
60 | /** |
61 | * @var string |
62 | */ |
63 | protected $suffix; |
64 | |
65 | /** |
66 | * @var Language |
67 | */ |
68 | public $lang; |
69 | |
70 | /** |
71 | * @var null|bool |
72 | */ |
73 | public $custom; |
74 | |
75 | private LocalisationCache $localisationCache; |
76 | |
77 | /** |
78 | * @param IContextSource $context |
79 | * @param Language $contentLanguage |
80 | * @param LanguageFactory $languageFactory |
81 | * @param LinkRenderer $linkRenderer |
82 | * @param IConnectionProvider $dbProvider |
83 | * @param LocalisationCache $localisationCache |
84 | * @param FormOptions $opts |
85 | */ |
86 | public function __construct( |
87 | IContextSource $context, |
88 | Language $contentLanguage, |
89 | LanguageFactory $languageFactory, |
90 | LinkRenderer $linkRenderer, |
91 | IConnectionProvider $dbProvider, |
92 | LocalisationCache $localisationCache, |
93 | FormOptions $opts |
94 | ) { |
95 | // Set database before parent constructor to avoid setting it there |
96 | $this->mDb = $dbProvider->getReplicaDatabase(); |
97 | parent::__construct( $context, $linkRenderer ); |
98 | $this->localisationCache = $localisationCache; |
99 | |
100 | $this->mIndexField = 'am_title'; |
101 | // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? |
102 | $this->mDefaultDirection = IndexPager::DIR_DESCENDING; |
103 | |
104 | $this->lang = $languageFactory->getRawLanguage( $opts->getValue( 'lang' ) ); |
105 | |
106 | $this->foreign = !$this->lang->equals( $contentLanguage ); |
107 | |
108 | $filter = $opts->getValue( 'filter' ); |
109 | if ( $filter === 'all' ) { |
110 | $this->custom = null; // So won't match in either case |
111 | } else { |
112 | $this->custom = ( $filter === 'unmodified' ); |
113 | } |
114 | |
115 | $prefix = $this->getLanguage()->ucfirst( $opts->getValue( 'prefix' ) ); |
116 | $prefix = $prefix !== '' ? |
117 | Title::makeTitleSafe( NS_MEDIAWIKI, $opts->getValue( 'prefix' ) ) : |
118 | null; |
119 | |
120 | if ( $prefix !== null ) { |
121 | $displayPrefix = $prefix->getDBkey(); |
122 | $this->prefix = '/^' . preg_quote( $displayPrefix, '/' ) . '/i'; |
123 | } else { |
124 | $this->prefix = false; |
125 | } |
126 | |
127 | // The suffix that may be needed for message names if we're in a |
128 | // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' |
129 | if ( $this->foreign ) { |
130 | $this->suffix = '/' . $this->lang->getCode(); |
131 | } else { |
132 | $this->suffix = ''; |
133 | } |
134 | } |
135 | |
136 | private function getAllMessages( $descending ) { |
137 | $messageNames = $this->localisationCache->getSubitemList( 'en', 'messages' ); |
138 | |
139 | // Normalise message names so they look like page titles and sort correctly - T86139 |
140 | $messageNames = array_map( [ $this->lang, 'ucfirst' ], $messageNames ); |
141 | |
142 | if ( $descending ) { |
143 | rsort( $messageNames ); |
144 | } else { |
145 | asort( $messageNames ); |
146 | } |
147 | |
148 | return $messageNames; |
149 | } |
150 | |
151 | /** |
152 | * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. |
153 | * Returns [ 'pages' => ..., 'talks' => ... ], where the subarrays have |
154 | * an entry for each existing page, with the key being the message name and |
155 | * value arbitrary. |
156 | * |
157 | * @since 1.36 Added $dbr parameter |
158 | * |
159 | * @param array $messageNames |
160 | * @param string $langcode What language code |
161 | * @param bool $foreign Whether the $langcode is not the content language |
162 | * @param IReadableDatabase|null $dbr |
163 | * @return array A 'pages' and 'talks' array with the keys of existing pages |
164 | */ |
165 | public static function getCustomisedStatuses( |
166 | $messageNames, |
167 | $langcode = 'en', |
168 | $foreign = false, |
169 | ?IReadableDatabase $dbr = null |
170 | ) { |
171 | // FIXME: This function should be moved to Language:: or something. |
172 | // Fallback to global state, if not provided |
173 | $dbr ??= MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
174 | $res = $dbr->newSelectQueryBuilder() |
175 | ->select( [ 'page_namespace', 'page_title' ] ) |
176 | ->from( 'page' ) |
177 | ->where( [ 'page_namespace' => [ NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ] ] ) |
178 | ->useIndex( 'page_name_title' ) |
179 | ->caller( __METHOD__ )->fetchResultSet(); |
180 | $xNames = array_fill_keys( $messageNames, true ); |
181 | |
182 | $pageFlags = $talkFlags = []; |
183 | |
184 | foreach ( $res as $s ) { |
185 | $exists = false; |
186 | |
187 | if ( $foreign ) { |
188 | $titleParts = explode( '/', $s->page_title ); |
189 | if ( count( $titleParts ) === 2 && |
190 | $langcode === $titleParts[1] && |
191 | isset( $xNames[$titleParts[0]] ) |
192 | ) { |
193 | $exists = $titleParts[0]; |
194 | } |
195 | } elseif ( isset( $xNames[$s->page_title] ) ) { |
196 | $exists = $s->page_title; |
197 | } |
198 | |
199 | $title = Title::newFromRow( $s ); |
200 | if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { |
201 | $pageFlags[$exists] = true; |
202 | } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { |
203 | $talkFlags[$exists] = true; |
204 | } |
205 | } |
206 | |
207 | return [ 'pages' => $pageFlags, 'talks' => $talkFlags ]; |
208 | } |
209 | |
210 | /** |
211 | * This function normally does a database query to get the results; we need |
212 | * to make a pretend result using a FakeResultWrapper. |
213 | * @param string $offset |
214 | * @param int $limit |
215 | * @param bool $order |
216 | * @return IResultWrapper |
217 | */ |
218 | public function reallyDoQuery( $offset, $limit, $order ) { |
219 | $asc = ( $order === self::QUERY_ASCENDING ); |
220 | |
221 | $messageNames = $this->getAllMessages( $order ); |
222 | $statuses = self::getCustomisedStatuses( |
223 | $messageNames, |
224 | $this->lang->getCode(), |
225 | $this->foreign, |
226 | $this->getDatabase() |
227 | ); |
228 | |
229 | $rows = []; |
230 | $count = 0; |
231 | foreach ( $messageNames as $key ) { |
232 | $customised = isset( $statuses['pages'][$key] ); |
233 | if ( $customised !== $this->custom && |
234 | ( ( $asc && ( $key < $offset || !$offset ) ) || ( !$asc && $key > $offset ) ) && |
235 | ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) |
236 | ) { |
237 | $actual = $this->msg( $key )->inLanguage( $this->lang )->plain(); |
238 | $default = $this->msg( $key )->inLanguage( $this->lang )->useDatabase( false )->plain(); |
239 | $rows[] = [ |
240 | 'am_title' => $key, |
241 | 'am_actual' => $actual, |
242 | 'am_default' => $default, |
243 | 'am_customised' => $customised, |
244 | 'am_talk_exists' => isset( $statuses['talks'][$key] ) |
245 | ]; |
246 | $count++; |
247 | } |
248 | |
249 | if ( $count === $limit ) { |
250 | break; |
251 | } |
252 | } |
253 | |
254 | return new FakeResultWrapper( $rows ); |
255 | } |
256 | |
257 | protected function getStartBody() { |
258 | return Xml::openElement( 'table', [ |
259 | 'class' => $this->getTableClass(), |
260 | 'id' => 'mw-allmessagestable' |
261 | ] ) . |
262 | "\n" . |
263 | "<thead><tr> |
264 | <th rowspan=\"2\">" . |
265 | $this->msg( 'allmessagesname' )->escaped() . " |
266 | </th> |
267 | <th>" . |
268 | $this->msg( 'allmessagesdefault' )->escaped() . |
269 | "</th> |
270 | </tr>\n |
271 | <tr> |
272 | <th>" . |
273 | $this->msg( 'allmessagescurrent' )->escaped() . |
274 | "</th> |
275 | </tr></thead>\n"; |
276 | } |
277 | |
278 | protected function getEndBody() { |
279 | return Html::closeElement( 'table' ); |
280 | } |
281 | |
282 | /** |
283 | * @param string $field |
284 | * @param string|null $value |
285 | * @return string HTML |
286 | */ |
287 | public function formatValue( $field, $value ) { |
288 | $linkRenderer = $this->getLinkRenderer(); |
289 | switch ( $field ) { |
290 | case 'am_title': |
291 | $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); |
292 | $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); |
293 | $message = $this->msg( $value )->inLanguage( $this->lang )->useDatabase( false )->plain(); |
294 | $translation = $linkRenderer->makeExternalLink( |
295 | 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( [ |
296 | 'title' => 'Special:SearchTranslations', |
297 | 'group' => 'mediawiki', |
298 | 'grouppath' => 'mediawiki', |
299 | 'language' => $this->lang->getCode(), |
300 | 'query' => $value . ' ' . $message |
301 | ] ), |
302 | $this->msg( 'allmessages-filter-translate' ), |
303 | $this->getTitle() |
304 | ); |
305 | $talkLink = $this->msg( 'talkpagelinktext' )->text(); |
306 | |
307 | if ( $this->mCurrentRow->am_customised ) { |
308 | $title = $linkRenderer->makeKnownLink( $title, $this->getLanguage()->lcfirst( $value ) ); |
309 | } else { |
310 | $title = $linkRenderer->makeBrokenLink( |
311 | $title, $this->getLanguage()->lcfirst( $value ) |
312 | ); |
313 | } |
314 | if ( $this->mCurrentRow->am_talk_exists ) { |
315 | $talk = $linkRenderer->makeKnownLink( $talk, $talkLink ); |
316 | } else { |
317 | $talk = $linkRenderer->makeBrokenLink( |
318 | $talk, |
319 | $talkLink |
320 | ); |
321 | } |
322 | |
323 | return $title . ' ' . |
324 | $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . |
325 | ' ' . |
326 | $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); |
327 | |
328 | case 'am_default': |
329 | case 'am_actual': |
330 | return Sanitizer::escapeHtmlAllowEntities( $value ); |
331 | } |
332 | |
333 | return ''; |
334 | } |
335 | |
336 | /** |
337 | * @param stdClass $row |
338 | * @return string HTML |
339 | */ |
340 | public function formatRow( $row ) { |
341 | // Do all the normal stuff |
342 | $s = parent::formatRow( $row ); |
343 | |
344 | // But if there's a customised message, add that too. |
345 | if ( $row->am_customised ) { |
346 | $s .= Html::openElement( 'tr', $this->getRowAttrs( $row ) ); |
347 | $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); |
348 | |
349 | if ( $formatted === '' ) { |
350 | $formatted = "\u{00A0}"; |
351 | } |
352 | |
353 | $s .= Html::rawElement( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) |
354 | . Html::closeElement( 'tr' ); |
355 | } |
356 | |
357 | return Html::rawElement( 'tbody', [], $s ); |
358 | } |
359 | |
360 | protected function getRowAttrs( $row ) { |
361 | return []; |
362 | } |
363 | |
364 | /** |
365 | * @param string $field |
366 | * @param string $value |
367 | * @return array HTML attributes |
368 | */ |
369 | protected function getCellAttrs( $field, $value ) { |
370 | $attr = []; |
371 | if ( $field === 'am_title' ) { |
372 | if ( $this->mCurrentRow->am_customised ) { |
373 | $attr += [ 'rowspan' => '2' ]; |
374 | } |
375 | } else { |
376 | $attr += [ |
377 | 'lang' => $this->lang->getHtmlCode(), |
378 | 'dir' => $this->lang->getDir(), |
379 | ]; |
380 | if ( $this->mCurrentRow->am_customised ) { |
381 | // CSS class: am_default, am_actual |
382 | $attr += [ 'class' => $field ]; |
383 | } |
384 | } |
385 | return $attr; |
386 | } |
387 | |
388 | // This is not actually used, as getStartBody is overridden above |
389 | protected function getFieldNames() { |
390 | return [ |
391 | 'am_title' => $this->msg( 'allmessagesname' )->text(), |
392 | 'am_default' => $this->msg( 'allmessagesdefault' )->text() |
393 | ]; |
394 | } |
395 | |
396 | public function getTitle() { |
397 | return SpecialPage::getTitleFor( 'Allmessages', false ); |
398 | } |
399 | |
400 | protected function isFieldSortable( $x ) { |
401 | return false; |
402 | } |
403 | |
404 | public function getDefaultSort() { |
405 | return ''; |
406 | } |
407 | |
408 | public function getQueryInfo() { |
409 | return []; |
410 | } |
411 | |
412 | } |
413 | |
414 | /** |
415 | * Retain the old class name for backwards compatibility. |
416 | * @deprecated since 1.41 |
417 | */ |
418 | class_alias( AllMessagesTablePager::class, 'AllMessagesTablePager' ); |