MediaWiki 1.41.2
ImageListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
24use HTMLForm;
26use LocalRepo;
36use MWException;
37use RepoGroup;
38use UserCache;
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 with wfGetDB
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
325 public function reallyDoQuery( $offset, $limit, $order ) {
326 $dbr = $this->getDatabase();
327 $prevTableName = $this->mTableName;
328 $this->mTableName = 'image';
329 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
330 $this->buildQueryInfo( $offset, $limit, $order );
331 $imageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
332 $this->mTableName = $prevTableName;
333
334 if ( !$this->mShowAll ) {
335 return $imageRes;
336 }
337
338 $this->mTableName = 'oldimage';
339
340 # Hacky...
341 $oldIndex = $this->mIndexField;
342 foreach ( $this->mIndexField as &$index ) {
343 if ( !str_starts_with( $index, 'img_' ) ) {
344 throw new MWException( "Expected to be sorting on an image table field" );
345 }
346 $index = 'oi_' . substr( $index, 4 );
347 }
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
441 public function formatValue( $field, $value ) {
442 $linkRenderer = $this->getLinkRenderer();
443 switch ( $field ) {
444 case 'thumb':
445 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
446 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
447 // If statement for paranoia
448 if ( $file ) {
449 $thumb = $file->transform( [ 'width' => 180, 'height' => 360, 'loading' => 'lazy' ] );
450 if ( $thumb ) {
451 return $thumb->toHtml( [ 'desc-link' => true ] );
452 } else {
453 return $this->msg( 'thumbnail_error', '' )->escaped();
454 }
455 } else {
456 return htmlspecialchars( $this->getCurrentRow()->img_name );
457 }
458 case 'img_timestamp':
459 // We may want to make this a link to the "old" version when displaying old files
460 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
461 case 'img_name':
462 static $imgfile = null;
463 if ( $imgfile === null ) {
464 $imgfile = $this->msg( 'imgfile' )->text();
465 }
466
467 // Weird files can maybe exist? T24227
468 $filePage = Title::makeTitleSafe( NS_FILE, $value );
469 if ( $filePage ) {
470 $html = $linkRenderer->makeKnownLink(
471 $filePage,
472 $filePage->getText()
473 );
474 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
475 $file = $this->localRepo->findFile( $value, $opt );
476 if ( $file ) {
477 $download = Xml::element(
478 'a',
479 [ 'href' => $file->getUrl() ],
480 $imgfile
481 );
482 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $download )->escaped();
483 }
484
485 // Add delete links if allowed
486 // From https://github.com/Wikia/app/pull/3859
487 if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
488 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
489
490 $delete = $linkRenderer->makeKnownLink(
491 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
492 );
493 $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
494 }
495
496 return $html;
497 } else {
498 return htmlspecialchars( $value );
499 }
500 case 'img_actor':
501 if ( $this->mCurrentRow->actor_user ) {
502 $name = $this->mCurrentRow->actor_name;
503 $link = $linkRenderer->makeLink(
504 Title::makeTitle( NS_USER, $name ),
505 $name
506 );
507 } else {
508 $link = $value !== null ? htmlspecialchars( $value ) : '';
509 }
510
511 return $link;
512 case 'img_size':
513 return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
514 case 'img_description':
515 $field = $this->mCurrentRow->description_field;
516 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
517 return $this->commentFormatter->format( $value );
518 case 'count':
519 return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
520 case 'top':
521 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
522 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
523 default:
524 throw new MWException( "Unknown field '$field'" );
525 }
526 }
527
532 private function getEscapedLimitSelectList(): array {
533 $list = $this->getLimitSelectList();
534 $result = [];
535 foreach ( $list as $key => $value ) {
536 $result[htmlspecialchars( $key )] = $value;
537 }
538 return $result;
539 }
540
541 public function getForm() {
542 $formDescriptor = [];
543 $formDescriptor['limit'] = [
544 'type' => 'radio',
545 'name' => 'limit',
546 'label-message' => 'table_pager_limit_label',
547 'options' => $this->getEscapedLimitSelectList(),
548 'flatlist' => true,
549 'default' => $this->mLimit
550 ];
551
552 $formDescriptor['user'] = [
553 'type' => 'user',
554 'name' => 'user',
555 'id' => 'mw-listfiles-user',
556 'label-message' => 'username',
557 'default' => $this->mUserName,
558 'size' => '40',
559 'maxlength' => '255',
560 ];
561
562 $formDescriptor['ilshowall'] = [
563 'type' => 'check',
564 'name' => 'ilshowall',
565 'id' => 'mw-listfiles-show-all',
566 'label-message' => 'listfiles-show-all',
567 'default' => $this->mShowAll,
568 ];
569
570 $query = $this->getRequest()->getQueryValues();
571 unset( $query['title'] );
572 unset( $query['limit'] );
573 unset( $query['ilsearch'] );
574 unset( $query['ilshowall'] );
575 unset( $query['user'] );
576
577 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
578 ->setMethod( 'get' )
579 ->setId( 'mw-listfiles-form' )
580 ->setTitle( $this->getTitle() )
581 ->setSubmitTextMsg( 'listfiles-pager-submit' )
582 ->setWrapperLegendMsg( 'listfiles' )
583 ->addHiddenFields( $query )
584 ->prepareForm()
585 ->displayForm( '' );
586 }
587
588 protected function getTableClass() {
589 return parent::getTableClass() . ' listfiles';
590 }
591
592 protected function getNavClass() {
593 return parent::getNavClass() . ' listfiles_nav';
594 }
595
596 protected function getSortHeaderClass() {
597 return parent::getSortHeaderClass() . ' listfiles_sort';
598 }
599
600 public function getPagingQueries() {
601 $queries = parent::getPagingQueries();
602 if ( $this->mUserName !== null ) {
603 # Append the username to the query string
604 foreach ( $queries as &$query ) {
605 if ( $query !== false ) {
606 $query['user'] = $this->mUserName;
607 }
608 }
609 }
610
611 return $queries;
612 }
613
614 public function getDefaultQuery() {
615 $queries = parent::getDefaultQuery();
616 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
617 $queries['user'] = $this->mUserName;
618 }
619
620 return $queries;
621 }
622
623 public function getTitle() {
624 return SpecialPage::getTitleFor( 'Listfiles' );
625 }
626}
627
632class_alias( ImageListPager::class, 'ImageListPager' );
getUser()
getAuthority()
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
setContext(IContextSource $context)
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:158
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:45
MediaWiki exception.
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.
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
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:76
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
internal since 1.36
Definition User.php:98
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.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42