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->select( $tables, $fields, $conds, $fname, $options, $join_conds );
331 $this->mTableName = $prevTableName;
332
333 if ( !$this->mShowAll ) {
334 return $imageRes;
335 }
336
337 $this->mTableName = 'oldimage';
338
339 # Hacky...
340 $oldIndex = $this->mIndexField;
341 foreach ( $this->mIndexField as &$index ) {
342 if ( !str_starts_with( $index, 'img_' ) ) {
343 throw new UnexpectedValueException( "Expected to be sorting on an image table field" );
344 }
345 $index = 'oi_' . substr( $index, 4 );
346 }
347 unset( $index );
348
349 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
350 $this->buildQueryInfo( $offset, $limit, $order );
351 $oldimageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
352
353 $this->mTableName = $prevTableName;
354 $this->mIndexField = $oldIndex;
355
356 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
357 }
358
370 protected function combineResult( $res1, $res2, $limit, $order ) {
371 $res1->rewind();
372 $res2->rewind();
373 $topRes1 = $res1->fetchObject();
374 $topRes2 = $res2->fetchObject();
375 $resultArray = [];
376 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
377 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
378 if ( $order !== IndexPager::QUERY_ASCENDING ) {
379 $resultArray[] = $topRes1;
380 $topRes1 = $res1->fetchObject();
381 } else {
382 $resultArray[] = $topRes2;
383 $topRes2 = $res2->fetchObject();
384 }
385 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
386 $resultArray[] = $topRes2;
387 $topRes2 = $res2->fetchObject();
388 } else {
389 $resultArray[] = $topRes1;
390 $topRes1 = $res1->fetchObject();
391 }
392 }
393
394 for ( ; $i < $limit && $topRes1; $i++ ) {
395 $resultArray[] = $topRes1;
396 $topRes1 = $res1->fetchObject();
397 }
398
399 for ( ; $i < $limit && $topRes2; $i++ ) {
400 $resultArray[] = $topRes2;
401 $topRes2 = $res2->fetchObject();
402 }
403
404 return new FakeResultWrapper( $resultArray );
405 }
406
407 public function getIndexField() {
408 return [ self::INDEX_FIELDS[$this->mSort] ];
409 }
410
411 public function getDefaultSort() {
412 if ( $this->mShowAll &&
413 $this->getConfig()->get( MainConfigNames::MiserMode ) &&
414 $this->mUserName === null
415 ) {
416 // Unfortunately no index on oi_timestamp.
417 return 'img_name';
418 } else {
419 return 'img_timestamp';
420 }
421 }
422
423 protected function doBatchLookups() {
424 $userIds = [];
425 $this->mResult->seek( 0 );
426 foreach ( $this->mResult as $row ) {
427 if ( $row->actor_user ) {
428 $userIds[] = $row->actor_user;
429 }
430 }
431 # Do a link batch query for names and userpages
432 $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
433 }
434
440 public function formatValue( $field, $value ) {
441 $linkRenderer = $this->getLinkRenderer();
442 switch ( $field ) {
443 case 'thumb':
444 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
445 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
446 // If statement for paranoia
447 if ( $file ) {
448 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
449 if ( $thumb ) {
450 return $thumb->toHtml( [ 'desc-link' => true, 'loading' => 'lazy' ] );
451 }
452 return $this->msg( 'thumbnail_error', '' )->escaped();
453 } else {
454 return htmlspecialchars( $this->getCurrentRow()->img_name );
455 }
456 case 'img_timestamp':
457 // We may want to make this a link to the "old" version when displaying old files
458 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
459 case 'img_name':
460 static $imgfile = null;
461 if ( $imgfile === null ) {
462 $imgfile = $this->msg( 'imgfile' )->text();
463 }
464
465 // Weird files can maybe exist? T24227
466 $filePage = Title::makeTitleSafe( NS_FILE, $value );
467 if ( $filePage ) {
468 $html = $linkRenderer->makeKnownLink(
469 $filePage,
470 $filePage->getText()
471 );
472 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
473 $file = $this->localRepo->findFile( $value, $opt );
474 if ( $file ) {
475 $download = Xml::element(
476 'a',
477 [ 'href' => $file->getUrl() ],
478 $imgfile
479 );
480 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $download )->escaped();
481 }
482
483 // Add delete links if allowed
484 // From https://github.com/Wikia/app/pull/3859
485 if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
486 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
487
488 $delete = $linkRenderer->makeKnownLink(
489 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
490 );
491 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
492 }
493
494 return $html;
495 } else {
496 return htmlspecialchars( $value );
497 }
498 case 'img_actor':
499 if ( $this->mCurrentRow->actor_user ) {
500 $name = $this->mCurrentRow->actor_name;
501 $link = $linkRenderer->makeLink(
502 Title::makeTitle( NS_USER, $name ),
503 $name
504 );
505 } else {
506 $link = $value !== null ? htmlspecialchars( $value ) : '';
507 }
508
509 return $link;
510 case 'img_size':
511 return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
512 case 'img_description':
513 $field = $this->mCurrentRow->description_field;
514 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
515 return $this->commentFormatter->format( $value );
516 case 'count':
517 return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
518 case 'top':
519 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
520 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
521 default:
522 throw new UnexpectedValueException( "Unknown field '$field'" );
523 }
524 }
525
530 private function getEscapedLimitSelectList(): array {
531 $list = $this->getLimitSelectList();
532 $result = [];
533 foreach ( $list as $key => $value ) {
534 $result[htmlspecialchars( $key )] = $value;
535 }
536 return $result;
537 }
538
539 public function getForm() {
540 $formDescriptor = [];
541 $formDescriptor['limit'] = [
542 'type' => 'radio',
543 'name' => 'limit',
544 'label-message' => 'table_pager_limit_label',
545 'options' => $this->getEscapedLimitSelectList(),
546 'flatlist' => true,
547 'default' => $this->mLimit
548 ];
549
550 $formDescriptor['user'] = [
551 'type' => 'user',
552 'name' => 'user',
553 'id' => 'mw-listfiles-user',
554 'label-message' => 'username',
555 'default' => $this->mUserName,
556 'size' => '40',
557 'maxlength' => '255',
558 ];
559
560 $formDescriptor['ilshowall'] = [
561 'type' => 'check',
562 'name' => 'ilshowall',
563 'id' => 'mw-listfiles-show-all',
564 'label-message' => 'listfiles-show-all',
565 'default' => $this->mShowAll,
566 ];
567
568 $query = $this->getRequest()->getQueryValues();
569 unset( $query['title'] );
570 unset( $query['limit'] );
571 unset( $query['ilsearch'] );
572 unset( $query['ilshowall'] );
573 unset( $query['user'] );
574
575 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
576 ->setMethod( 'get' )
577 ->setId( 'mw-listfiles-form' )
578 ->setTitle( $this->getTitle() )
579 ->setSubmitTextMsg( 'listfiles-pager-submit' )
580 ->setWrapperLegendMsg( 'listfiles' )
581 ->addHiddenFields( $query )
582 ->prepareForm()
583 ->displayForm( '' );
584 }
585
586 protected function getTableClass() {
587 return parent::getTableClass() . ' listfiles';
588 }
589
590 protected function getNavClass() {
591 return parent::getNavClass() . ' listfiles_nav';
592 }
593
594 protected function getSortHeaderClass() {
595 return parent::getSortHeaderClass() . ' listfiles_sort';
596 }
597
598 public function getPagingQueries() {
599 $queries = parent::getPagingQueries();
600 if ( $this->mUserName !== null ) {
601 # Append the username to the query string
602 foreach ( $queries as &$query ) {
603 if ( $query !== false ) {
604 $query['user'] = $this->mUserName;
605 }
606 }
607 }
608
609 return $queries;
610 }
611
612 public function getDefaultQuery() {
613 $queries = parent::getDefaultQuery();
614 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
615 $queries['user'] = $this->mUserName;
616 }
617
618 return $queries;
619 }
620
621 public function getTitle() {
622 return SpecialPage::getTitleFor( 'Listfiles' );
623 }
624}
625
630class_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.