MediaWiki  master
ImageListPager.php
Go to the documentation of this file.
1 <?php
32 
36 class ImageListPager extends TablePager {
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)
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:352
__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)
Definition: IndexPager.php:78
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
Definition: IndexPager.php:474
string string[] $mIndexField
The index to actually be used for ordering.
Definition: IndexPager.php:110
getDatabase()
Get the Database object in use.
Definition: IndexPager.php:248
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:80
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
Definition: IndexPager.php:83
MediaWiki exception.
Definition: MWException.php:32
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.
Definition: RepoGroup.php:343
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.
Definition: TablePager.php:30
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...
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
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.
const DB_REPLICA
Definition: defines.php:26
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42