Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 162 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
AllMessagesTablePager | |
0.00% |
0 / 161 |
|
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 / 35 |
|
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 Language; |
25 | use LocalisationCache; |
26 | use MediaWiki\Context\IContextSource; |
27 | use MediaWiki\Html\FormOptions; |
28 | use MediaWiki\Html\Html; |
29 | use MediaWiki\Languages\LanguageFactory; |
30 | use MediaWiki\Linker\Linker; |
31 | use MediaWiki\Linker\LinkRenderer; |
32 | use MediaWiki\MediaWikiServices; |
33 | use MediaWiki\Parser\Sanitizer; |
34 | use MediaWiki\SpecialPage\SpecialPage; |
35 | use MediaWiki\Title\Title; |
36 | use stdClass; |
37 | use Wikimedia\Rdbms\FakeResultWrapper; |
38 | use Wikimedia\Rdbms\IConnectionProvider; |
39 | use Wikimedia\Rdbms\IReadableDatabase; |
40 | use Wikimedia\Rdbms\IResultWrapper; |
41 | use Xml; |
42 | |
43 | /** |
44 | * Use TablePager for prettified output. We have to pretend that we're |
45 | * getting data from a table when in fact not all of it comes from the database. |
46 | * |
47 | * @ingroup Pager |
48 | */ |
49 | class AllMessagesTablePager extends TablePager { |
50 | |
51 | /** |
52 | * @var bool |
53 | */ |
54 | protected $foreign; |
55 | |
56 | /** |
57 | * @var string|false |
58 | */ |
59 | protected $prefix; |
60 | |
61 | /** |
62 | * @var string |
63 | */ |
64 | protected $suffix; |
65 | |
66 | /** |
67 | * @var Language |
68 | */ |
69 | public $lang; |
70 | |
71 | /** |
72 | * @var null|bool |
73 | */ |
74 | public $custom; |
75 | |
76 | private LocalisationCache $localisationCache; |
77 | |
78 | /** |
79 | * @param IContextSource $context |
80 | * @param Language $contentLanguage |
81 | * @param LanguageFactory $languageFactory |
82 | * @param LinkRenderer $linkRenderer |
83 | * @param IConnectionProvider $dbProvider |
84 | * @param LocalisationCache $localisationCache |
85 | * @param FormOptions $opts |
86 | */ |
87 | public function __construct( |
88 | IContextSource $context, |
89 | Language $contentLanguage, |
90 | LanguageFactory $languageFactory, |
91 | LinkRenderer $linkRenderer, |
92 | IConnectionProvider $dbProvider, |
93 | LocalisationCache $localisationCache, |
94 | FormOptions $opts |
95 | ) { |
96 | // Set database before parent constructor to avoid setting it there |
97 | $this->mDb = $dbProvider->getReplicaDatabase(); |
98 | parent::__construct( $context, $linkRenderer ); |
99 | $this->localisationCache = $localisationCache; |
100 | |
101 | $this->mIndexField = 'am_title'; |
102 | // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? |
103 | $this->mDefaultDirection = IndexPager::DIR_DESCENDING; |
104 | |
105 | $this->lang = $languageFactory->getRawLanguage( $opts->getValue( 'lang' ) ); |
106 | |
107 | $this->foreign = !$this->lang->equals( $contentLanguage ); |
108 | |
109 | $filter = $opts->getValue( 'filter' ); |
110 | if ( $filter === 'all' ) { |
111 | $this->custom = null; // So won't match in either case |
112 | } else { |
113 | $this->custom = ( $filter === 'unmodified' ); |
114 | } |
115 | |
116 | $prefix = $this->getLanguage()->ucfirst( $opts->getValue( 'prefix' ) ); |
117 | $prefix = $prefix !== '' ? |
118 | Title::makeTitleSafe( NS_MEDIAWIKI, $opts->getValue( 'prefix' ) ) : |
119 | null; |
120 | |
121 | if ( $prefix !== null ) { |
122 | $displayPrefix = $prefix->getDBkey(); |
123 | $this->prefix = '/^' . preg_quote( $displayPrefix, '/' ) . '/i'; |
124 | } else { |
125 | $this->prefix = false; |
126 | } |
127 | |
128 | // The suffix that may be needed for message names if we're in a |
129 | // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' |
130 | if ( $this->foreign ) { |
131 | $this->suffix = '/' . $this->lang->getCode(); |
132 | } else { |
133 | $this->suffix = ''; |
134 | } |
135 | } |
136 | |
137 | private function getAllMessages( $descending ) { |
138 | $messageNames = $this->localisationCache->getSubitemList( 'en', 'messages' ); |
139 | |
140 | // Normalise message names so they look like page titles and sort correctly - T86139 |
141 | $messageNames = array_map( [ $this->lang, 'ucfirst' ], $messageNames ); |
142 | |
143 | if ( $descending ) { |
144 | rsort( $messageNames ); |
145 | } else { |
146 | asort( $messageNames ); |
147 | } |
148 | |
149 | return $messageNames; |
150 | } |
151 | |
152 | /** |
153 | * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. |
154 | * Returns [ 'pages' => ..., 'talks' => ... ], where the subarrays have |
155 | * an entry for each existing page, with the key being the message name and |
156 | * value arbitrary. |
157 | * |
158 | * @since 1.36 Added $dbr parameter |
159 | * |
160 | * @param array $messageNames |
161 | * @param string $langcode What language code |
162 | * @param bool $foreign Whether the $langcode is not the content language |
163 | * @param IReadableDatabase|null $dbr |
164 | * @return array A 'pages' and 'talks' array with the keys of existing pages |
165 | */ |
166 | public static function getCustomisedStatuses( |
167 | $messageNames, |
168 | $langcode = 'en', |
169 | $foreign = false, |
170 | IReadableDatabase $dbr = null |
171 | ) { |
172 | // FIXME: This function should be moved to Language:: or something. |
173 | // Fallback to global state, if not provided |
174 | $dbr ??= MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
175 | $res = $dbr->newSelectQueryBuilder() |
176 | ->select( [ 'page_namespace', 'page_title' ] ) |
177 | ->from( 'page' ) |
178 | ->where( [ 'page_namespace' => [ NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ] ] ) |
179 | ->useIndex( 'page_name_title' ) |
180 | ->caller( __METHOD__ )->fetchResultSet(); |
181 | $xNames = array_fill_keys( $messageNames, true ); |
182 | |
183 | $pageFlags = $talkFlags = []; |
184 | |
185 | foreach ( $res as $s ) { |
186 | $exists = false; |
187 | |
188 | if ( $foreign ) { |
189 | $titleParts = explode( '/', $s->page_title ); |
190 | if ( count( $titleParts ) === 2 && |
191 | $langcode === $titleParts[1] && |
192 | isset( $xNames[$titleParts[0]] ) |
193 | ) { |
194 | $exists = $titleParts[0]; |
195 | } |
196 | } elseif ( isset( $xNames[$s->page_title] ) ) { |
197 | $exists = $s->page_title; |
198 | } |
199 | |
200 | $title = Title::newFromRow( $s ); |
201 | if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { |
202 | $pageFlags[$exists] = true; |
203 | } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { |
204 | $talkFlags[$exists] = true; |
205 | } |
206 | } |
207 | |
208 | return [ 'pages' => $pageFlags, 'talks' => $talkFlags ]; |
209 | } |
210 | |
211 | /** |
212 | * This function normally does a database query to get the results; we need |
213 | * to make a pretend result using a FakeResultWrapper. |
214 | * @param string $offset |
215 | * @param int $limit |
216 | * @param bool $order |
217 | * @return IResultWrapper |
218 | */ |
219 | public function reallyDoQuery( $offset, $limit, $order ) { |
220 | $asc = ( $order === self::QUERY_ASCENDING ); |
221 | |
222 | $messageNames = $this->getAllMessages( $order ); |
223 | $statuses = self::getCustomisedStatuses( |
224 | $messageNames, |
225 | $this->lang->getCode(), |
226 | $this->foreign, |
227 | $this->getDatabase() |
228 | ); |
229 | |
230 | $rows = []; |
231 | $count = 0; |
232 | foreach ( $messageNames as $key ) { |
233 | $customised = isset( $statuses['pages'][$key] ); |
234 | if ( $customised !== $this->custom && |
235 | ( ( $asc && ( $key < $offset || !$offset ) ) || ( !$asc && $key > $offset ) ) && |
236 | ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) |
237 | ) { |
238 | $actual = $this->msg( $key )->inLanguage( $this->lang )->plain(); |
239 | $default = $this->msg( $key )->inLanguage( $this->lang )->useDatabase( false )->plain(); |
240 | $rows[] = [ |
241 | 'am_title' => $key, |
242 | 'am_actual' => $actual, |
243 | 'am_default' => $default, |
244 | 'am_customised' => $customised, |
245 | 'am_talk_exists' => isset( $statuses['talks'][$key] ) |
246 | ]; |
247 | $count++; |
248 | } |
249 | |
250 | if ( $count === $limit ) { |
251 | break; |
252 | } |
253 | } |
254 | |
255 | return new FakeResultWrapper( $rows ); |
256 | } |
257 | |
258 | protected function getStartBody() { |
259 | return Xml::openElement( 'table', [ |
260 | 'class' => $this->getTableClass(), |
261 | 'id' => 'mw-allmessagestable' |
262 | ] ) . |
263 | "\n" . |
264 | "<thead><tr> |
265 | <th rowspan=\"2\">" . |
266 | $this->msg( 'allmessagesname' )->escaped() . " |
267 | </th> |
268 | <th>" . |
269 | $this->msg( 'allmessagesdefault' )->escaped() . |
270 | "</th> |
271 | </tr>\n |
272 | <tr> |
273 | <th>" . |
274 | $this->msg( 'allmessagescurrent' )->escaped() . |
275 | "</th> |
276 | </tr></thead>\n"; |
277 | } |
278 | |
279 | protected function getEndBody() { |
280 | return Html::closeElement( 'table' ); |
281 | } |
282 | |
283 | /** |
284 | * @param string $field |
285 | * @param string|null $value |
286 | * @return string HTML |
287 | */ |
288 | public function formatValue( $field, $value ) { |
289 | $linkRenderer = $this->getLinkRenderer(); |
290 | switch ( $field ) { |
291 | case 'am_title': |
292 | $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); |
293 | $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); |
294 | $message = $this->msg( $value )->inLanguage( $this->lang )->useDatabase( false )->plain(); |
295 | $translation = Linker::makeExternalLink( |
296 | 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( [ |
297 | 'title' => 'Special:SearchTranslations', |
298 | 'group' => 'mediawiki', |
299 | 'grouppath' => 'mediawiki', |
300 | 'language' => $this->lang->getCode(), |
301 | 'query' => $value . ' ' . $message |
302 | ] ), |
303 | $this->msg( 'allmessages-filter-translate' )->text() |
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' ); |