MediaWiki 1.40.4
ImageListPager.php
Go to the documentation of this file.
1<?php
32
37
38 protected $mFieldNames = null;
39
40 // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
41 protected $mQueryConds = [];
42
43 protected $mUserName = null;
44
50 protected $mUser = null;
51
52 protected $mIncluding = false;
53
54 protected $mShowAll = false;
55
56 protected $mTableName = 'image';
57
59 private $commentStore;
60
62 private $localRepo;
63
65 private $userCache;
66
68 private $commentFormatter;
69
73 private const INDEX_FIELDS = [
74 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
75 'img_name' => [ 'img_name' ],
76 'img_size' => [ 'img_size', 'img_name' ],
77 ];
78
93 public function __construct(
94 IContextSource $context,
95 CommentStore $commentStore,
96 LinkRenderer $linkRenderer,
97 ILoadBalancer $loadBalancer,
98 RepoGroup $repoGroup,
99 UserCache $userCache,
100 UserNameUtils $userNameUtils,
101 CommentFormatter $commentFormatter,
102 $userName,
103 $search,
104 $including,
105 $showAll
106 ) {
107 $this->setContext( $context );
108
109 $this->mIncluding = $including;
110 $this->mShowAll = $showAll;
111 $dbr = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
112
113 if ( $userName !== null && $userName !== '' ) {
114 $nt = Title::makeTitleSafe( NS_USER, $userName );
115 if ( $nt === null ) {
116 $this->outputUserDoesNotExist( $userName );
117 } else {
118 $this->mUserName = $nt->getText();
119 $user = User::newFromName( $this->mUserName, false );
120 if ( $user ) {
121 $this->mUser = $user;
122 }
123 if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
124 $this->outputUserDoesNotExist( $userName );
125 }
126 }
127 }
128
129 if ( !$including ) {
130 if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
131 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
132 } else {
133 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
134 }
135 } else {
136 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
137 }
138 // Set database before parent constructor to avoid setting it there with wfGetDB
139 $this->mDb = $dbr;
140
141 parent::__construct( $context, $linkRenderer );
142 $this->commentStore = $commentStore;
143 $this->localRepo = $repoGroup->getLocalRepo();
144 $this->userCache = $userCache;
145 $this->commentFormatter = $commentFormatter;
146 }
147
153 public function getRelevantUser() {
154 return $this->mUser;
155 }
156
162 protected function outputUserDoesNotExist( $userName ) {
163 $this->getOutput()->addHTML( Html::warningBox(
164 $this->getOutput()->msg( 'listfiles-userdoesnotexist', wfEscapeWikiText( $userName ) )->parse(),
165 'mw-userpage-userdoesnotexist'
166 ) );
167 }
168
176 protected function buildQueryConds( $table ) {
177 $conds = [];
178
179 if ( $this->mUserName !== null ) {
180 // getQueryInfoReal() should have handled the tables and joins.
181 $conds['actor_name'] = $this->mUserName;
182 }
183
184 if ( $table === 'oldimage' ) {
185 // Don't want to deal with revdel.
186 // Future fixme: Show partial information as appropriate.
187 // Would have to be careful about filtering by username when username is deleted.
188 $conds['oi_deleted'] = 0;
189 }
190
191 // Add mQueryConds in case anyone was subclassing and using the old variable.
192 return $conds + $this->mQueryConds;
193 }
194
195 protected function getFieldNames() {
196 if ( !$this->mFieldNames ) {
197 $this->mFieldNames = [
198 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
199 'img_name' => $this->msg( 'listfiles_name' )->text(),
200 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
201 'img_size' => $this->msg( 'listfiles_size' )->text(),
202 ];
203 if ( $this->mUserName === null ) {
204 // Do not show username if filtering by username
205 $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
206 }
207 // img_description down here, in order so that its still after the username field.
208 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
209
210 if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) && !$this->mShowAll ) {
211 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
212 }
213 if ( $this->mShowAll ) {
214 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->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 ) && $this->mUserName !== null ) {
233 // If we're sorting by user, the index only supports sorting by time.
234 return $field === 'img_timestamp';
235 } elseif ( $this->getConfig()->get( MainConfigNames::MiserMode )
236 && $this->mShowAll /* && mUserName === null */
237 ) {
238 // no oi_timestamp index, so only alphabetical sorting in this case.
239 return $field === 'img_name';
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 $qi = $this->getQueryInfoReal( $this->mTableName );
250
251 return $qi;
252 }
253
264 protected function getQueryInfoReal( $table ) {
265 $dbr = $this->getDatabase();
266 $prefix = $table === 'oldimage' ? 'oi' : 'img';
267
268 $tables = [ $table, 'actor' ];
269 $join_conds = [];
270
271 if ( $table === 'oldimage' ) {
272 $fields = [
273 'img_timestamp' => 'oi_timestamp',
274 'img_name' => 'oi_name',
275 'img_size' => 'oi_size',
276 'top' => $dbr->addQuotes( 'no' )
277 ];
278 $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
279 } else {
280 $fields = [
281 'img_timestamp',
282 'img_name',
283 'img_size',
284 'top' => $dbr->addQuotes( 'yes' )
285 ];
286 $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
287 }
288
289 $options = [];
290
291 # Description field
292 $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
293 $tables += $commentQuery['tables'];
294 $fields += $commentQuery['fields'];
295 $join_conds += $commentQuery['joins'];
296 $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
297
298 # Actor fields
299 $fields[] = 'actor_user';
300 $fields[] = 'actor_name';
301
302 # Depends on $wgMiserMode
303 # Will also not happen if mShowAll is true.
304 if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
305 $fields['count'] = $dbr->buildSelectSubquery(
306 'oldimage',
307 'COUNT(oi_archive_name)',
308 'oi_name = img_name',
309 __METHOD__
310 );
311 }
312
313 return [
314 'tables' => $tables,
315 'fields' => $fields,
316 'conds' => $this->buildQueryConds( $table ),
317 'options' => $options,
318 'join_conds' => $join_conds
319 ];
320 }
321
331 public function reallyDoQuery( $offset, $limit, $order ) {
332 $dbr = $this->getDatabase();
333 $prevTableName = $this->mTableName;
334 $this->mTableName = 'image';
335 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
336 $this->buildQueryInfo( $offset, $limit, $order );
337 $imageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
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 MWException( "Expected to be sorting on an image table field" );
351 }
352 $index = 'oi_' . substr( $index, 4 );
353 }
354
355 [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
356 $this->buildQueryInfo( $offset, $limit, $order );
357 $oldimageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
358
359 $this->mTableName = $prevTableName;
360 $this->mIndexField = $oldIndex;
361
362 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
363 }
364
376 protected function combineResult( $res1, $res2, $limit, $order ) {
377 $res1->rewind();
378 $res2->rewind();
379 $topRes1 = $res1->fetchObject();
380 $topRes2 = $res2->fetchObject();
381 $resultArray = [];
382 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
383 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
384 if ( $order !== IndexPager::QUERY_ASCENDING ) {
385 $resultArray[] = $topRes1;
386 $topRes1 = $res1->fetchObject();
387 } else {
388 $resultArray[] = $topRes2;
389 $topRes2 = $res2->fetchObject();
390 }
391 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
392 $resultArray[] = $topRes2;
393 $topRes2 = $res2->fetchObject();
394 } else {
395 $resultArray[] = $topRes1;
396 $topRes1 = $res1->fetchObject();
397 }
398 }
399
400 for ( ; $i < $limit && $topRes1; $i++ ) {
401 $resultArray[] = $topRes1;
402 $topRes1 = $res1->fetchObject();
403 }
404
405 for ( ; $i < $limit && $topRes2; $i++ ) {
406 $resultArray[] = $topRes2;
407 $topRes2 = $res2->fetchObject();
408 }
409
410 return new FakeResultWrapper( $resultArray );
411 }
412
413 public function getIndexField() {
414 return [ self::INDEX_FIELDS[$this->mSort] ];
415 }
416
417 public function getDefaultSort() {
418 if ( $this->mShowAll && $this->getConfig()->get( MainConfigNames::MiserMode ) &&
419 $this->mUserName === null ) {
420 // Unfortunately no index on oi_timestamp.
421 return 'img_name';
422 } else {
423 return 'img_timestamp';
424 }
425 }
426
427 protected function doBatchLookups() {
428 $userIds = [];
429 $this->mResult->seek( 0 );
430 foreach ( $this->mResult as $row ) {
431 if ( $row->actor_user ) {
432 $userIds[] = $row->actor_user;
433 }
434 }
435 # Do a link batch query for names and userpages
436 $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
437 }
438
445 public function formatValue( $field, $value ) {
446 $linkRenderer = $this->getLinkRenderer();
447 switch ( $field ) {
448 case 'thumb':
449 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
450 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
451 // If statement for paranoia
452 if ( $file ) {
453 $thumb = $file->transform( [ 'width' => 180, 'height' => 360, 'loading' => 'lazy' ] );
454 if ( $thumb ) {
455 return $thumb->toHtml( [ 'desc-link' => true ] );
456 } else {
457 return $this->msg( 'thumbnail_error', '' )->escaped();
458 }
459 } else {
460 return htmlspecialchars( $this->getCurrentRow()->img_name );
461 }
462 case 'img_timestamp':
463 // We may want to make this a link to the "old" version when displaying old files
464 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
465 case 'img_name':
466 static $imgfile = null;
467 if ( $imgfile === null ) {
468 $imgfile = $this->msg( 'imgfile' )->text();
469 }
470
471 // Weird files can maybe exist? T24227
472 $filePage = Title::makeTitleSafe( NS_FILE, $value );
473 if ( $filePage ) {
474 $link = $linkRenderer->makeKnownLink(
475 $filePage,
476 $filePage->getText()
477 );
478 $download = Xml::element(
479 'a',
480 [ 'href' => $this->localRepo->newFile( $filePage )->getUrl() ],
481 $imgfile
482 );
483 $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
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 $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
494
495 return "$link $download $delete";
496 }
497
498 return "$link $download";
499 } else {
500 return htmlspecialchars( $value );
501 }
502 case 'img_actor':
503 if ( $this->mCurrentRow->actor_user ) {
504 $name = $this->mCurrentRow->actor_name;
505 $link = $linkRenderer->makeLink(
506 Title::makeTitle( NS_USER, $name ),
507 $name
508 );
509 } else {
510 $link = $value !== null ? htmlspecialchars( $value ) : '';
511 }
512
513 return $link;
514 case 'img_size':
515 return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
516 case 'img_description':
517 $field = $this->mCurrentRow->description_field;
518 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
519 return $this->commentFormatter->format( $value );
520 case 'count':
521 return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
522 case 'top':
523 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
524 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
525 default:
526 throw new MWException( "Unknown field '$field'" );
527 }
528 }
529
530 public function getForm() {
531 $formDescriptor = [];
532 $formDescriptor['limit'] = [
533 'type' => 'select',
534 'name' => 'limit',
535 'label-message' => 'table_pager_limit_label',
536 'options' => $this->getLimitSelectList(),
537 'default' => $this->mLimit,
538 ];
539
540 $formDescriptor['user'] = [
541 'type' => 'user',
542 'name' => 'user',
543 'id' => 'mw-listfiles-user',
544 'label-message' => 'username',
545 'default' => $this->mUserName,
546 'size' => '40',
547 'maxlength' => '255',
548 ];
549
550 $formDescriptor['ilshowall'] = [
551 'type' => 'check',
552 'name' => 'ilshowall',
553 'id' => 'mw-listfiles-show-all',
554 'label-message' => 'listfiles-show-all',
555 'default' => $this->mShowAll,
556 ];
557
558 $query = $this->getRequest()->getQueryValues();
559 unset( $query['title'] );
560 unset( $query['limit'] );
561 unset( $query['ilsearch'] );
562 unset( $query['ilshowall'] );
563 unset( $query['user'] );
564
565 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
566 ->setMethod( 'get' )
567 ->setId( 'mw-listfiles-form' )
568 ->setTitle( $this->getTitle() )
569 ->setSubmitTextMsg( 'table_pager_limit_submit' )
570 ->setWrapperLegendMsg( 'listfiles' )
571 ->addHiddenFields( $query )
572 ->prepareForm()
573 ->displayForm( '' );
574 }
575
576 protected function getTableClass() {
577 return parent::getTableClass() . ' listfiles';
578 }
579
580 protected function getNavClass() {
581 return parent::getNavClass() . ' listfiles_nav';
582 }
583
584 protected function getSortHeaderClass() {
585 return parent::getSortHeaderClass() . ' listfiles_sort';
586 }
587
588 public function getPagingQueries() {
589 $queries = parent::getPagingQueries();
590 if ( $this->mUserName !== null ) {
591 # Append the username to the query string
592 foreach ( $queries as &$query ) {
593 if ( $query !== false ) {
594 $query['user'] = $this->mUserName;
595 }
596 }
597 }
598
599 return $queries;
600 }
601
602 public function getDefaultQuery() {
603 $queries = parent::getDefaultQuery();
604 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
605 $queries['user'] = $this->mUserName;
606 }
607
608 return $queries;
609 }
610
611 public function getTitle() {
612 return SpecialPage::getTitleFor( 'Listfiles' );
613 }
614}
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)
__construct(IContextSource $context, CommentStore $commentStore, LinkRenderer $linkRenderer, ILoadBalancer $loadBalancer, RepoGroup $repoGroup, UserCache $userCache, UserNameUtils $userNameUtils, CommentFormatter $commentFormatter, $userName, $search, $including, $showAll)
getDefaultSort()
The database field name used as a default sort order.
getTableClass()
TablePager relies on mw-datatable for styling, see T214208.
formatValue( $field, $value)
outputUserDoesNotExist( $userName)
Add a message to the output stating that the user doesn't exist.
getFieldNames()
An array mapping database field names to a textual description of the field name, for use in the tabl...
combineResult( $res1, $res2, $limit, $order)
Combine results from 2 tables.
buildQueryConds( $table)
Build the where clause of the query.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
User null $mUser
The relevant user.
getIndexField()
Returns the name of the index field.If the pager supports multiple orders, it may return an array of ...
getRelevantUser()
Get the user relevant to the ImageList.
getQueryInfo()
Provides all parameters needed for the main paged query.
reallyDoQuery( $offset, $limit, $order)
Override reallyDoQuery to mix together two queries.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
getPagingQueries()
Get a URL query array for the prev, next, first and last links.
getQueryInfoReal( $table)
Actually get the query info.
isFieldSortable( $field)
Return true if the named field should be sortable by the UI, false otherwise.
const DIR_ASCENDING
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.
getDatabase()
Get the Database object in use.
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:41
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:55
Class that generates HTML for internal links.
A class containing constants representing the names of configuration variables.
Represents a title within MediaWiki.
Definition Title.php:82
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
Prioritized list of file repositories.
Definition RepoGroup.php:30
getLocalRepo()
Get the local repository, i.e.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Table-based display with a user-selectable sort order.
internal since 1.36
Definition User.php:71
static newFromName( $name, $validate='valid')
Definition User.php:592
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.
This class is a delegate to ILBFactory for a given database cluster.
getConnectionRef( $i, $groups=[], $domain=false, $flags=0)
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