MediaWiki master
ImageListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use LocalRepo;
37use RepoGroup;
38use UnexpectedValueException;
42use Xml;
43
48
50 protected $mFieldNames = null;
55 protected $mQueryConds = [];
57 protected $mUserName = null;
59 protected $mUser = null;
61 protected $mIncluding = false;
63 protected $mShowAll = false;
65 protected $mTableName = 'image';
66
67 private CommentStore $commentStore;
68 private LocalRepo $localRepo;
69 private UserCache $userCache;
70 private CommentFormatter $commentFormatter;
71
75 private const INDEX_FIELDS = [
76 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
77 'img_name' => [ 'img_name' ],
78 'img_size' => [ 'img_size', 'img_name' ],
79 ];
80
95 public function __construct(
96 IContextSource $context,
97 CommentStore $commentStore,
98 LinkRenderer $linkRenderer,
99 IConnectionProvider $dbProvider,
100 RepoGroup $repoGroup,
101 UserCache $userCache,
102 UserNameUtils $userNameUtils,
103 CommentFormatter $commentFormatter,
104 $userName,
105 $search,
106 $including,
107 $showAll
108 ) {
109 $this->setContext( $context );
110
111 $this->mIncluding = $including;
112 $this->mShowAll = $showAll;
113
114 if ( $userName !== null && $userName !== '' ) {
115 $nt = Title::makeTitleSafe( NS_USER, $userName );
116 if ( $nt === null ) {
117 $this->outputUserDoesNotExist( $userName );
118 } else {
119 $this->mUserName = $nt->getText();
120 $user = User::newFromName( $this->mUserName, false );
121 if ( $user ) {
122 $this->mUser = $user;
123 }
124 if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
125 $this->outputUserDoesNotExist( $userName );
126 }
127 }
128 }
129
130 if ( $including ||
131 $this->getRequest()->getText( 'sort', 'img_date' ) === 'img_date'
132 ) {
133 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
134 } else {
135 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
136 }
137 // Set database before parent constructor to avoid setting it there
138 $this->mDb = $dbProvider->getReplicaDatabase();
139
140 parent::__construct( $context, $linkRenderer );
141 $this->commentStore = $commentStore;
142 $this->localRepo = $repoGroup->getLocalRepo();
143 $this->userCache = $userCache;
144 $this->commentFormatter = $commentFormatter;
145 }
146
152 public function getRelevantUser() {
153 return $this->mUser;
154 }
155
161 protected function outputUserDoesNotExist( $userName ) {
162 $this->getOutput()->addHTML( Html::warningBox(
163 $this->getOutput()->msg( 'listfiles-userdoesnotexist', wfEscapeWikiText( $userName ) )->parse(),
164 'mw-userpage-userdoesnotexist'
165 ) );
166 }
167
175 protected function buildQueryConds( $table ) {
176 $conds = [];
177
178 if ( $this->mUserName !== null ) {
179 // getQueryInfoReal() should have handled the tables and joins.
180 $conds['actor_name'] = $this->mUserName;
181 }
182
183 if ( $table === 'oldimage' ) {
184 // Don't want to deal with revdel.
185 // Future fixme: Show partial information as appropriate.
186 // Would have to be careful about filtering by username when username is deleted.
187 $conds['oi_deleted'] = 0;
188 }
189
190 // Add mQueryConds in case anyone was subclassing and using the old variable.
191 return $conds + $this->mQueryConds;
192 }
193
194 protected function getFieldNames() {
195 if ( !$this->mFieldNames ) {
196 $this->mFieldNames = [
197 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
198 'img_name' => $this->msg( 'listfiles_name' )->text(),
199 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
200 'img_size' => $this->msg( 'listfiles_size' )->text(),
201 ];
202 if ( $this->mUserName === null ) {
203 // Do not show username if filtering by username
204 $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
205 }
206 // img_description down here, in order so that its still after the username field.
207 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
208
209 if ( $this->mShowAll ) {
210 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
211 } elseif ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
212 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
213 }
214 }
215
216 return $this->mFieldNames;
217 }
218
219 protected function isFieldSortable( $field ) {
220 if ( $this->mIncluding ) {
221 return false;
222 }
223 /* For reference, the indices we can use for sorting are:
224 * On the image table: img_actor_timestamp, img_size, img_timestamp
225 * On oldimage: oi_actor_timestamp, oi_name_timestamp
226 *
227 * In particular that means we cannot sort by timestamp when not filtering
228 * by user and including old images in the results. Which is sad. (T279982)
229 */
230 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
231 if ( $this->mUserName !== null ) {
232 // If we're sorting by user, the index only supports sorting by time.
233 return $field === 'img_timestamp';
234 } elseif ( $this->mShowAll ) {
235 // no oi_timestamp index, so only alphabetical sorting in this case.
236 return $field === 'img_name';
237 }
238 }
239
240 return isset( self::INDEX_FIELDS[$field] );
241 }
242
243 public function getQueryInfo() {
244 // Hacky Hacky Hacky - I want to get query info
245 // for two different tables, without reimplementing
246 // the pager class.
247 return $this->getQueryInfoReal( $this->mTableName );
248 }
249
260 protected function getQueryInfoReal( $table ) {
261 $dbr = $this->getDatabase();
262 $prefix = $table === 'oldimage' ? 'oi' : 'img';
263
264 $tables = [ $table, 'actor' ];
265 $join_conds = [];
266
267 if ( $table === 'oldimage' ) {
268 $fields = [
269 'img_timestamp' => 'oi_timestamp',
270 'img_name' => 'oi_name',
271 'img_size' => 'oi_size',
272 'top' => $dbr->addQuotes( 'no' )
273 ];
274 $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
275 } else {
276 $fields = [
277 'img_timestamp',
278 'img_name',
279 'img_size',
280 'top' => $dbr->addQuotes( 'yes' )
281 ];
282 $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
283 }
284
285 # Description field
286 $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
287 $tables += $commentQuery['tables'];
288 $fields += $commentQuery['fields'];
289 $join_conds += $commentQuery['joins'];
290 $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
291
292 # Actor fields
293 $fields[] = 'actor_user';
294 $fields[] = 'actor_name';
295
296 # Depends on $wgMiserMode
297 # Will also not happen if mShowAll is true.
298 if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
299 $fields['count'] = $dbr->buildSelectSubquery(
300 'oldimage',
301 'COUNT(oi_archive_name)',
302 'oi_name = img_name',
303 __METHOD__
304 );
305 }
306
307 return [
308 'tables' => $tables,
309 'fields' => $fields,
310 'conds' => $this->buildQueryConds( $table ),
311 'options' => [],
312 'join_conds' => $join_conds
313 ];
314 }
315
324 public function reallyDoQuery( $offset, $limit, $order ) {
325 $dbr = $this->getDatabase();
326 $prevTableName = $this->mTableName;
327 $this->mTableName = 'image';
328 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
329 $this->buildQueryInfo( $offset, $limit, $order );
330 $imageRes = $dbr->newSelectQueryBuilder()
331 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
332 ->fields( $fields )
333 ->conds( $conds )
334 ->caller( $fname )
335 ->options( $options )
336 ->joinConds( $join_conds )
337 ->fetchResultSet();
338 $this->mTableName = $prevTableName;
339
340 if ( !$this->mShowAll ) {
341 return $imageRes;
342 }
343
344 $this->mTableName = 'oldimage';
345
346 # Hacky...
347 $oldIndex = $this->mIndexField;
348 foreach ( $this->mIndexField as &$index ) {
349 if ( !str_starts_with( $index, 'img_' ) ) {
350 throw new UnexpectedValueException( "Expected to be sorting on an image table field" );
351 }
352 $index = 'oi_' . substr( $index, 4 );
353 }
354 unset( $index );
355
356 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
357 $this->buildQueryInfo( $offset, $limit, $order );
358 $oldimageRes = $dbr->newSelectQueryBuilder()
359 ->tables( is_array( $tables ) ? $tables : [ $tables ] )
360 ->fields( $fields )
361 ->conds( $conds )
362 ->caller( $fname )
363 ->options( $options )
364 ->joinConds( $join_conds )
365 ->fetchResultSet();
366
367 $this->mTableName = $prevTableName;
368 $this->mIndexField = $oldIndex;
369
370 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
371 }
372
384 protected function combineResult( $res1, $res2, $limit, $order ) {
385 $res1->rewind();
386 $res2->rewind();
387 $topRes1 = $res1->fetchObject();
388 $topRes2 = $res2->fetchObject();
389 $resultArray = [];
390 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
391 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
392 if ( $order !== IndexPager::QUERY_ASCENDING ) {
393 $resultArray[] = $topRes1;
394 $topRes1 = $res1->fetchObject();
395 } else {
396 $resultArray[] = $topRes2;
397 $topRes2 = $res2->fetchObject();
398 }
399 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
400 $resultArray[] = $topRes2;
401 $topRes2 = $res2->fetchObject();
402 } else {
403 $resultArray[] = $topRes1;
404 $topRes1 = $res1->fetchObject();
405 }
406 }
407
408 for ( ; $i < $limit && $topRes1; $i++ ) {
409 $resultArray[] = $topRes1;
410 $topRes1 = $res1->fetchObject();
411 }
412
413 for ( ; $i < $limit && $topRes2; $i++ ) {
414 $resultArray[] = $topRes2;
415 $topRes2 = $res2->fetchObject();
416 }
417
418 return new FakeResultWrapper( $resultArray );
419 }
420
421 public function getIndexField() {
422 return [ self::INDEX_FIELDS[$this->mSort] ];
423 }
424
425 public function getDefaultSort() {
426 if ( $this->mShowAll &&
427 $this->getConfig()->get( MainConfigNames::MiserMode ) &&
428 $this->mUserName === null
429 ) {
430 // Unfortunately no index on oi_timestamp.
431 return 'img_name';
432 } else {
433 return 'img_timestamp';
434 }
435 }
436
437 protected function doBatchLookups() {
438 $userIds = [];
439 $this->mResult->seek( 0 );
440 foreach ( $this->mResult as $row ) {
441 if ( $row->actor_user ) {
442 $userIds[] = $row->actor_user;
443 }
444 }
445 # Do a link batch query for names and userpages
446 $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
447 }
448
454 public function formatValue( $field, $value ) {
455 $linkRenderer = $this->getLinkRenderer();
456 switch ( $field ) {
457 case 'thumb':
458 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
459 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
460 // If statement for paranoia
461 if ( $file ) {
462 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
463 if ( $thumb ) {
464 return $thumb->toHtml( [ 'desc-link' => true, 'loading' => 'lazy' ] );
465 }
466 return $this->msg( 'thumbnail_error', '' )->escaped();
467 } else {
468 return htmlspecialchars( $this->getCurrentRow()->img_name );
469 }
470 case 'img_timestamp':
471 // We may want to make this a link to the "old" version when displaying old files
472 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
473 case 'img_name':
474 static $imgfile = null;
475 if ( $imgfile === null ) {
476 $imgfile = $this->msg( 'imgfile' )->text();
477 }
478
479 // Weird files can maybe exist? T24227
480 $filePage = Title::makeTitleSafe( NS_FILE, $value );
481 if ( $filePage ) {
482 $html = $linkRenderer->makeKnownLink(
483 $filePage,
484 $filePage->getText()
485 );
486 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
487 $file = $this->localRepo->findFile( $value, $opt );
488 if ( $file ) {
489 $download = Xml::element(
490 'a',
491 [ 'href' => $file->getUrl() ],
492 $imgfile
493 );
494 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $download )->escaped();
495 }
496
497 // Add delete links if allowed
498 // From https://github.com/Wikia/app/pull/3859
499 if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
500 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
501
502 $delete = $linkRenderer->makeKnownLink(
503 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
504 );
505 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
506 }
507
508 return $html;
509 } else {
510 return htmlspecialchars( $value );
511 }
512 case 'img_actor':
513 if ( $this->mCurrentRow->actor_user ) {
514 $name = $this->mCurrentRow->actor_name;
515 $link = $linkRenderer->makeLink(
516 Title::makeTitle( NS_USER, $name ),
517 $name
518 );
519 } else {
520 $link = $value !== null ? htmlspecialchars( $value ) : '';
521 }
522
523 return $link;
524 case 'img_size':
525 return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
526 case 'img_description':
527 $field = $this->mCurrentRow->description_field;
528 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
529 return $this->commentFormatter->format( $value );
530 case 'count':
531 return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
532 case 'top':
533 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
534 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
535 default:
536 throw new UnexpectedValueException( "Unknown field '$field'" );
537 }
538 }
539
544 private function getEscapedLimitSelectList(): array {
545 $list = $this->getLimitSelectList();
546 $result = [];
547 foreach ( $list as $key => $value ) {
548 $result[htmlspecialchars( $key )] = $value;
549 }
550 return $result;
551 }
552
553 public function getForm() {
554 $formDescriptor = [];
555 $formDescriptor['limit'] = [
556 'type' => 'radio',
557 'name' => 'limit',
558 'label-message' => 'table_pager_limit_label',
559 'options' => $this->getEscapedLimitSelectList(),
560 'flatlist' => true,
561 'default' => $this->mLimit
562 ];
563
564 $formDescriptor['user'] = [
565 'type' => 'user',
566 'name' => 'user',
567 'id' => 'mw-listfiles-user',
568 'label-message' => 'username',
569 'default' => $this->mUserName,
570 'size' => '40',
571 'maxlength' => '255',
572 ];
573
574 $formDescriptor['ilshowall'] = [
575 'type' => 'check',
576 'name' => 'ilshowall',
577 'id' => 'mw-listfiles-show-all',
578 'label-message' => 'listfiles-show-all',
579 'default' => $this->mShowAll,
580 ];
581
582 $query = $this->getRequest()->getQueryValues();
583 unset( $query['title'] );
584 unset( $query['limit'] );
585 unset( $query['ilsearch'] );
586 unset( $query['ilshowall'] );
587 unset( $query['user'] );
588
589 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
590 ->setMethod( 'get' )
591 ->setId( 'mw-listfiles-form' )
592 ->setTitle( $this->getTitle() )
593 ->setSubmitTextMsg( 'listfiles-pager-submit' )
594 ->setWrapperLegendMsg( 'listfiles' )
595 ->addHiddenFields( $query )
596 ->prepareForm()
597 ->displayForm( '' );
598 }
599
600 protected function getTableClass() {
601 return parent::getTableClass() . ' listfiles';
602 }
603
604 protected function getNavClass() {
605 return parent::getNavClass() . ' listfiles_nav';
606 }
607
608 protected function getSortHeaderClass() {
609 return parent::getSortHeaderClass() . ' listfiles_sort';
610 }
611
612 public function getPagingQueries() {
613 $queries = parent::getPagingQueries();
614 if ( $this->mUserName !== null ) {
615 # Append the username to the query string
616 foreach ( $queries as &$query ) {
617 if ( $query !== false ) {
618 $query['user'] = $this->mUserName;
619 }
620 }
621 }
622
623 return $queries;
624 }
625
626 public function getDefaultQuery() {
627 $queries = parent::getDefaultQuery();
628 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
629 $queries['user'] = $this->mUserName;
630 }
631
632 return $queries;
633 }
634
635 public function getTitle() {
636 return SpecialPage::getTitleFor( 'Listfiles' );
637 }
638}
639
644class_alias( ImageListPager::class, 'ImageListPager' );
getUser()
getRequest()
getAuthority()
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
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:49
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:206
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
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.
__construct(IContextSource $context, CommentStore $commentStore, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, RepoGroup $repoGroup, UserCache $userCache, UserNameUtils $userNameUtils, CommentFormatter $commentFormatter, $userName, $search, $including, $showAll)
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.
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:78
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
internal since 1.36
Definition User.php:93
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...
Module of static functions for generating XML.
Definition Xml.php:33
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.