MediaWiki  master
ImageListPager.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Pager;
23 
24 use HTMLForm;
25 use IContextSource;
26 use LocalRepo;
36 use MWException;
37 use RepoGroup;
38 use UserCache;
42 use Xml;
43 
47 class ImageListPager extends TablePager {
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 
632 class_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
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:360
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition: LocalRepo.php:45
MediaWiki exception.
Definition: MWException.php:33
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
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:823
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.
Definition: IndexPager.php:256
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
Definition: IndexPager.php:89
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:84
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:86
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
Definition: IndexPager.php:482
string string[] $mIndexField
The index to actually be used for ordering.
Definition: IndexPager.php:116
Table-based display with a user-selectable sort order.
Definition: TablePager.php:36
Parent class for all special pages.
Definition: SpecialPage.php:65
Represents a title within MediaWiki.
Definition: Title.php:76
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
internal since 1.36
Definition: User.php:98
static newFromName( $name, $validate='valid')
Definition: User.php:600
Prioritized list of file repositories.
Definition: RepoGroup.php:30
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:343
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
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:50
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