MediaWiki master
ImageListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use LocalRepo;
39use RepoGroup;
40use UnexpectedValueException;
45
50
52 protected $mFieldNames = null;
57 protected $mQueryConds = [];
59 protected $mUserName = null;
61 protected $mUser = null;
63 protected $mIncluding = false;
65 protected $mShowAll = false;
67 protected $mTableName = 'image';
68
69 private CommentStore $commentStore;
70 private LocalRepo $localRepo;
71 private CommentFormatter $commentFormatter;
72 private LinkBatchFactory $linkBatchFactory;
73
77 private const INDEX_FIELDS = [
78 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
79 'img_name' => [ 'img_name' ],
80 'img_size' => [ 'img_size', 'img_name' ],
81 ];
82
97 public function __construct(
98 IContextSource $context,
99 CommentStore $commentStore,
100 LinkRenderer $linkRenderer,
101 IConnectionProvider $dbProvider,
102 RepoGroup $repoGroup,
103 UserNameUtils $userNameUtils,
104 CommentFormatter $commentFormatter,
105 LinkBatchFactory $linkBatchFactory,
106 $userName,
107 $search,
108 $including,
109 $showAll
110 ) {
111 $this->setContext( $context );
112
113 $this->mIncluding = $including;
114 $this->mShowAll = $showAll;
115
116 if ( $userName !== null && $userName !== '' ) {
117 $nt = Title::makeTitleSafe( NS_USER, $userName );
118 if ( $nt === null ) {
119 $this->outputUserDoesNotExist( $userName );
120 } else {
121 $this->mUserName = $nt->getText();
122 $user = User::newFromName( $this->mUserName, false );
123 if ( $user ) {
124 $this->mUser = $user;
125 }
126 if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
127 $this->outputUserDoesNotExist( $userName );
128 }
129 }
130 }
131
132 if ( $including ||
133 $this->getRequest()->getText( 'sort', 'img_date' ) === 'img_date'
134 ) {
135 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
136 } else {
137 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
138 }
139 // Set database before parent constructor to avoid setting it there
140 $this->mDb = $dbProvider->getReplicaDatabase();
141
142 parent::__construct( $context, $linkRenderer );
143 $this->commentStore = $commentStore;
144 $this->localRepo = $repoGroup->getLocalRepo();
145 $this->commentFormatter = $commentFormatter;
146 $this->linkBatchFactory = $linkBatchFactory;
147 }
148
154 public function getRelevantUser() {
155 return $this->mUser;
156 }
157
163 protected function outputUserDoesNotExist( $userName ) {
164 $this->getOutput()->addHTML( Html::warningBox(
165 $this->getOutput()->msg( 'listfiles-userdoesnotexist', wfEscapeWikiText( $userName ) )->parse(),
166 'mw-userpage-userdoesnotexist'
167 ) );
168 }
169
177 protected function buildQueryConds( $table ) {
178 $conds = [];
179
180 if ( $this->mUserName !== null ) {
181 // getQueryInfoReal() should have handled the tables and joins.
182 $conds['actor_name'] = $this->mUserName;
183 }
184
185 if ( $table === 'oldimage' ) {
186 // Don't want to deal with revdel.
187 // Future fixme: Show partial information as appropriate.
188 // Would have to be careful about filtering by username when username is deleted.
189 $conds['oi_deleted'] = 0;
190 }
191
192 // Add mQueryConds in case anyone was subclassing and using the old variable.
193 return $conds + $this->mQueryConds;
194 }
195
196 protected function getFieldNames() {
197 if ( !$this->mFieldNames ) {
198 $this->mFieldNames = [
199 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
200 'img_name' => $this->msg( 'listfiles_name' )->text(),
201 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
202 'img_size' => $this->msg( 'listfiles_size' )->text(),
203 ];
204 if ( $this->mUserName === null ) {
205 // Do not show username if filtering by username
206 $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
207 }
208 // img_description down here, in order so that its still after the username field.
209 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
210
211 if ( $this->mShowAll ) {
212 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
213 } elseif ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
214 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
215 }
216 }
217
218 return $this->mFieldNames;
219 }
220
221 protected function isFieldSortable( $field ) {
222 if ( $this->mIncluding ) {
223 return false;
224 }
225 /* For reference, the indices we can use for sorting are:
226 * On the image table: img_actor_timestamp, img_size, img_timestamp
227 * On oldimage: oi_actor_timestamp, oi_name_timestamp
228 *
229 * In particular that means we cannot sort by timestamp when not filtering
230 * by user and including old images in the results. Which is sad. (T279982)
231 */
232 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
233 if ( $this->mUserName !== null ) {
234 // If we're sorting by user, the index only supports sorting by time.
235 return $field === 'img_timestamp';
236 } elseif ( $this->mShowAll ) {
237 // no oi_timestamp index, so only alphabetical sorting in this case.
238 return $field === 'img_name';
239 }
240 }
241
242 return isset( self::INDEX_FIELDS[$field] );
243 }
244
245 public function getQueryInfo() {
246 // Hacky Hacky Hacky - I want to get query info
247 // for two different tables, without reimplementing
248 // the pager class.
249 return $this->getQueryInfoReal( $this->mTableName );
250 }
251
262 protected function getQueryInfoReal( $table ) {
263 $dbr = $this->getDatabase();
264 $prefix = $table === 'oldimage' ? 'oi' : 'img';
265
266 $tables = [ $table, 'actor' ];
267 $join_conds = [];
268
269 if ( $table === 'oldimage' ) {
270 $fields = [
271 'img_timestamp' => 'oi_timestamp',
272 'img_name' => 'oi_name',
273 'img_size' => 'oi_size',
274 'top' => $dbr->addQuotes( 'no' )
275 ];
276 $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
277 } else {
278 $fields = [
279 'img_timestamp',
280 'img_name',
281 'img_size',
282 'top' => $dbr->addQuotes( 'yes' )
283 ];
284 $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
285 }
286
287 # Description field
288 $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
289 $tables += $commentQuery['tables'];
290 $fields += $commentQuery['fields'];
291 $join_conds += $commentQuery['joins'];
292 $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
293
294 # Actor fields
295 $fields[] = 'actor_user';
296 $fields[] = 'actor_name';
297
298 # Depends on $wgMiserMode
299 # Will also not happen if mShowAll is true.
300 if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
301 $fields['count'] = new Subquery( $dbr->newSelectQueryBuilder()
302 ->select( 'COUNT(oi_archive_name)' )
303 ->from( 'oldimage' )
304 ->where( 'oi_name = img_name' )
305 ->caller( __METHOD__ )
306 ->getSQL()
307 );
308 }
309
310 return [
311 'tables' => $tables,
312 'fields' => $fields,
313 'conds' => $this->buildQueryConds( $table ),
314 'options' => [],
315 'join_conds' => $join_conds
316 ];
317 }
318
327 public function reallyDoQuery( $offset, $limit, $order ) {
328 $dbr = $this->getDatabase();
329 $prevTableName = $this->mTableName;
330 $this->mTableName = 'image';
331 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
332 $this->buildQueryInfo( $offset, $limit, $order );
333 $imageRes = $dbr->newSelectQueryBuilder()
334 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
335 ->fields( $fields )
336 ->conds( $conds )
337 ->caller( $fname )
338 ->options( $options )
339 ->joinConds( $join_conds )
340 ->fetchResultSet();
341 $this->mTableName = $prevTableName;
342
343 if ( !$this->mShowAll ) {
344 return $imageRes;
345 }
346
347 $this->mTableName = 'oldimage';
348
349 # Hacky...
350 $oldIndex = $this->mIndexField;
351 foreach ( $this->mIndexField as &$index ) {
352 if ( !str_starts_with( $index, 'img_' ) ) {
353 throw new UnexpectedValueException( "Expected to be sorting on an image table field" );
354 }
355 $index = 'oi_' . substr( $index, 4 );
356 }
357 unset( $index );
358
359 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
360 $this->buildQueryInfo( $offset, $limit, $order );
361 $oldimageRes = $dbr->newSelectQueryBuilder()
362 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
363 ->fields( $fields )
364 ->conds( $conds )
365 ->caller( $fname )
366 ->options( $options )
367 ->joinConds( $join_conds )
368 ->fetchResultSet();
369
370 $this->mTableName = $prevTableName;
371 $this->mIndexField = $oldIndex;
372
373 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
374 }
375
387 protected function combineResult( $res1, $res2, $limit, $order ) {
388 $res1->rewind();
389 $res2->rewind();
390 $topRes1 = $res1->fetchObject();
391 $topRes2 = $res2->fetchObject();
392 $resultArray = [];
393 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
394 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
395 if ( $order !== IndexPager::QUERY_ASCENDING ) {
396 $resultArray[] = $topRes1;
397 $topRes1 = $res1->fetchObject();
398 } else {
399 $resultArray[] = $topRes2;
400 $topRes2 = $res2->fetchObject();
401 }
402 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
403 $resultArray[] = $topRes2;
404 $topRes2 = $res2->fetchObject();
405 } else {
406 $resultArray[] = $topRes1;
407 $topRes1 = $res1->fetchObject();
408 }
409 }
410
411 for ( ; $i < $limit && $topRes1; $i++ ) {
412 $resultArray[] = $topRes1;
413 $topRes1 = $res1->fetchObject();
414 }
415
416 for ( ; $i < $limit && $topRes2; $i++ ) {
417 $resultArray[] = $topRes2;
418 $topRes2 = $res2->fetchObject();
419 }
420
421 return new FakeResultWrapper( $resultArray );
422 }
423
424 public function getIndexField() {
425 return [ self::INDEX_FIELDS[$this->mSort] ];
426 }
427
428 public function getDefaultSort() {
429 if ( $this->mShowAll &&
430 $this->getConfig()->get( MainConfigNames::MiserMode ) &&
431 $this->mUserName === null
432 ) {
433 // Unfortunately no index on oi_timestamp.
434 return 'img_name';
435 } else {
436 return 'img_timestamp';
437 }
438 }
439
440 protected function doBatchLookups() {
441 $this->mResult->seek( 0 );
442 $batch = $this->linkBatchFactory->newLinkBatch();
443 foreach ( $this->mResult as $row ) {
444 $batch->add( NS_USER, $row->actor_name );
445 $batch->add( NS_USER_TALK, $row->actor_name );
446 $batch->add( NS_FILE, $row->img_name );
447 }
448 $batch->execute();
449 }
450
456 public function formatValue( $field, $value ) {
457 $linkRenderer = $this->getLinkRenderer();
458 switch ( $field ) {
459 case 'thumb':
460 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
461 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
462 // If statement for paranoia
463 if ( $file ) {
464 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
465 if ( $thumb ) {
466 return $thumb->toHtml( [ 'desc-link' => true, 'loading' => 'lazy' ] );
467 }
468 return $this->msg( 'thumbnail_error', '' )->escaped();
469 } else {
470 return htmlspecialchars( $this->getCurrentRow()->img_name );
471 }
472 case 'img_timestamp':
473 // We may want to make this a link to the "old" version when displaying old files
474 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
475 case 'img_name':
476 static $imgfile = null;
477 if ( $imgfile === null ) {
478 $imgfile = $this->msg( 'imgfile' )->text();
479 }
480
481 // Weird files can maybe exist? T24227
482 $filePage = Title::makeTitleSafe( NS_FILE, $value );
483 if ( $filePage ) {
484 $html = $linkRenderer->makeKnownLink(
485 $filePage,
486 $filePage->getText()
487 );
488 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
489 $file = $this->localRepo->findFile( $value, $opt );
490 if ( $file ) {
491 $download = Xml::element(
492 'a',
493 [ 'href' => $file->getUrl() ],
494 $imgfile
495 );
496 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $download )->escaped();
497 }
498
499 // Add delete links if allowed
500 // From https://github.com/Wikia/app/pull/3859
501 if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
502 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
503
504 $delete = $linkRenderer->makeKnownLink(
505 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
506 );
507 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
508 }
509
510 return $html;
511 } else {
512 return htmlspecialchars( $value );
513 }
514 case 'img_actor':
515 $userId = (int)$this->mCurrentRow->actor_user;
516 $userName = $this->mCurrentRow->actor_name;
517 return Linker::userLink( $userId, $userName )
518 . Linker::userToolLinks( $userId, $userName );
519 case 'img_size':
520 return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
521 case 'img_description':
522 $field = $this->mCurrentRow->description_field;
523 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
524 return $this->commentFormatter->format( $value );
525 case 'count':
526 return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
527 case 'top':
528 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
529 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
530 default:
531 throw new UnexpectedValueException( "Unknown field '$field'" );
532 }
533 }
534
539 private function getEscapedLimitSelectList(): array {
540 $list = $this->getLimitSelectList();
541 $result = [];
542 foreach ( $list as $key => $value ) {
543 $result[htmlspecialchars( $key )] = $value;
544 }
545 return $result;
546 }
547
548 public function getForm() {
549 $formDescriptor = [];
550 $formDescriptor['limit'] = [
551 'type' => 'radio',
552 'name' => 'limit',
553 'label-message' => 'table_pager_limit_label',
554 'options' => $this->getEscapedLimitSelectList(),
555 'flatlist' => true,
556 'default' => $this->mLimit
557 ];
558
559 $formDescriptor['user'] = [
560 'type' => 'user',
561 'name' => 'user',
562 'id' => 'mw-listfiles-user',
563 'label-message' => 'username',
564 'default' => $this->mUserName,
565 'size' => '40',
566 'maxlength' => '255',
567 ];
568
569 $formDescriptor['ilshowall'] = [
570 'type' => 'check',
571 'name' => 'ilshowall',
572 'id' => 'mw-listfiles-show-all',
573 'label-message' => 'listfiles-show-all',
574 'default' => $this->mShowAll,
575 ];
576
577 $query = $this->getRequest()->getQueryValues();
578 unset( $query['title'] );
579 unset( $query['limit'] );
580 unset( $query['ilsearch'] );
581 unset( $query['ilshowall'] );
582 unset( $query['user'] );
583
584 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
585 ->setMethod( 'get' )
586 ->setId( 'mw-listfiles-form' )
587 ->setTitle( $this->getTitle() )
588 ->setSubmitTextMsg( 'listfiles-pager-submit' )
589 ->setWrapperLegendMsg( 'listfiles' )
590 ->addHiddenFields( $query )
591 ->prepareForm()
592 ->displayForm( '' );
593 }
594
595 protected function getTableClass() {
596 return parent::getTableClass() . ' listfiles';
597 }
598
599 protected function getNavClass() {
600 return parent::getNavClass() . ' listfiles_nav';
601 }
602
603 protected function getSortHeaderClass() {
604 return parent::getSortHeaderClass() . ' listfiles_sort';
605 }
606
607 public function getPagingQueries() {
608 $queries = parent::getPagingQueries();
609 if ( $this->mUserName !== null ) {
610 # Append the username to the query string
611 foreach ( $queries as &$query ) {
612 if ( $query !== false ) {
613 $query['user'] = $this->mUserName;
614 }
615 }
616 }
617
618 return $queries;
619 }
620
621 public function getDefaultQuery() {
622 $queries = parent::getDefaultQuery();
623 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
624 $queries['user'] = $this->mUserName;
625 }
626
627 return $queries;
628 }
629
630 public function getTitle() {
631 return SpecialPage::getTitleFor( 'Listfiles' );
632 }
633}
634
639class_alias( ImageListPager::class, 'ImageListPager' );
getUser()
getRequest()
getAuthority()
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_USER_TALK
Definition Defines.php:68
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getContext()
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:48
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
setContext(IContextSource $context)
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:207
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:63
A class containing constants representing the names of configuration variables.
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
reallyDoQuery( $offset, $limit, $order)
Override reallyDoQuery to mix together two queries.
getDefaultSort()
The database field name used as a default sort order.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
getIndexField()
Returns the name of the index field.If the pager supports multiple orders, it may return an array of ...
combineResult( $res1, $res2, $limit, $order)
Combine results from 2 tables.
outputUserDoesNotExist( $userName)
Add a message to the output stating that the user doesn't exist.
buildQueryConds( $table)
Build the where clause of the query.
getQueryInfoReal( $table)
Actually get the query info.
isFieldSortable( $field)
Return true if the named field should be sortable by the UI, false otherwise.
User null $mUser
The relevant user.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
getFieldNames()
An array mapping database field names to a textual description of the field name, for use in the tabl...
getQueryInfo()
Provides all parameters needed for the main paged query.
__construct(IContextSource $context, CommentStore $commentStore, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, RepoGroup $repoGroup, UserNameUtils $userNameUtils, CommentFormatter $commentFormatter, LinkBatchFactory $linkBatchFactory, $userName, $search, $including, $showAll)
getTableClass()
TablePager relies on mw-datatable for styling, see T214208.
getPagingQueries()
Get a URL query array for the prev, next, first and last links.
getRelevantUser()
Get the user relevant to the ImageList.
getDatabase()
Get the Database object in use.
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
string string[] $mIndexField
The index to actually be used for ordering.
Table-based display with a user-selectable sort order.
Parent class for all special pages.
Represents a title within MediaWiki.
Definition Title.php:79
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
internal since 1.36
Definition User.php:93
Module of static functions for generating XML.
Definition Xml.php:37
Prioritized list of file repositories.
Definition RepoGroup.php:30
getLocalRepo()
Get the local repository, i.e.
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Provide primary and replica IDatabase connections.
getReplicaDatabase( $domain=false, $group=null)
Get connection to a replica database.
Result wrapper for grabbing data queried from an IDatabase object.