MediaWiki  master
ImageListPager.php
Go to the documentation of this file.
1 <?php
28 
32 class ImageListPager extends TablePager {
33 
34  protected $mFieldNames = null;
35 
36  // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
37  protected $mQueryConds = [];
38 
39  protected $mUserName = null;
40 
46  protected $mUser = null;
47 
48  protected $mIncluding = false;
49 
50  protected $mShowAll = false;
51 
52  protected $mTableName = 'image';
53 
55  private $commentStore;
56 
58  private $localRepo;
59 
61  private $userCache;
62 
66  private const INDEX_FIELDS = [
67  'img_timestamp' => [ 'img_timestamp', 'img_name' ],
68  'img_name' => [ 'img_name' ],
69  'img_size' => [ 'img_size', 'img_name' ],
70  ];
71 
85  public function __construct(
86  IContextSource $context,
87  CommentStore $commentStore,
88  LinkRenderer $linkRenderer,
89  ILoadBalancer $loadBalancer,
90  RepoGroup $repoGroup,
91  UserCache $userCache,
92  UserNameUtils $userNameUtils,
93  $userName,
94  $search,
95  $including,
96  $showAll
97  ) {
98  $this->setContext( $context );
99 
100  $this->mIncluding = $including;
101  $this->mShowAll = $showAll;
102  $dbr = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
103 
104  if ( $userName !== null && $userName !== '' ) {
105  $nt = Title::makeTitleSafe( NS_USER, $userName );
106  if ( $nt === null ) {
107  $this->outputUserDoesNotExist( $userName );
108  } else {
109  $this->mUserName = $nt->getText();
110  $user = User::newFromName( $this->mUserName, false );
111  if ( $user ) {
112  $this->mUser = $user;
113  }
114  if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
115  $this->outputUserDoesNotExist( $userName );
116  }
117  }
118  }
119 
120  if ( !$including ) {
121  if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
122  $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
123  } else {
124  $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
125  }
126  } else {
127  $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
128  }
129  // Set database before parent constructor to avoid setting it there with wfGetDB
130  $this->mDb = $dbr;
131 
132  parent::__construct( $context, $linkRenderer );
133  $this->commentStore = $commentStore;
134  $this->localRepo = $repoGroup->getLocalRepo();
135  $this->userCache = $userCache;
136  }
137 
143  public function getRelevantUser() {
144  return $this->mUser;
145  }
146 
152  protected function outputUserDoesNotExist( $userName ) {
153  $this->getOutput()->addHtml( Html::warningBox(
154  $this->getOutput()->msg( 'listfiles-userdoesnotexist', wfEscapeWikiText( $userName ) )->parse(),
155  'mw-userpage-userdoesnotexist'
156  ) );
157  }
158 
166  protected function buildQueryConds( $table ) {
167  $conds = [];
168 
169  if ( $this->mUserName !== null ) {
170  // getQueryInfoReal() should have handled the tables and joins.
171  $conds['actor_name'] = $this->mUserName;
172  }
173 
174  if ( $table === 'oldimage' ) {
175  // Don't want to deal with revdel.
176  // Future fixme: Show partial information as appropriate.
177  // Would have to be careful about filtering by username when username is deleted.
178  $conds['oi_deleted'] = 0;
179  }
180 
181  // Add mQueryConds in case anyone was subclassing and using the old variable.
182  return $conds + $this->mQueryConds;
183  }
184 
185  protected function getFieldNames() {
186  if ( !$this->mFieldNames ) {
187  $this->mFieldNames = [
188  'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
189  'img_name' => $this->msg( 'listfiles_name' )->text(),
190  'thumb' => $this->msg( 'listfiles_thumb' )->text(),
191  'img_size' => $this->msg( 'listfiles_size' )->text(),
192  ];
193  if ( $this->mUserName === null ) {
194  // Do not show username if filtering by username
195  $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
196  }
197  // img_description down here, in order so that its still after the username field.
198  $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
199 
200  if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) && !$this->mShowAll ) {
201  $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
202  }
203  if ( $this->mShowAll ) {
204  $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
205  }
206  }
207 
208  return $this->mFieldNames;
209  }
210 
211  protected function isFieldSortable( $field ) {
212  if ( $this->mIncluding ) {
213  return false;
214  }
215  $sortable = array_keys( self::INDEX_FIELDS );
216  /* For reference, the indices we can use for sorting are:
217  * On the image table: img_actor_timestamp, img_size, img_timestamp
218  * On oldimage: oi_actor_timestamp, oi_name_timestamp
219  *
220  * In particular that means we cannot sort by timestamp when not filtering
221  * by user and including old images in the results. Which is sad. (T279982)
222  */
223  if ( $this->getConfig()->get( MainConfigNames::MiserMode ) && $this->mUserName !== null ) {
224  // If we're sorting by user, the index only supports sorting by time.
225  return $field === 'img_timestamp';
226  } elseif ( $this->getConfig()->get( MainConfigNames::MiserMode )
227  && $this->mShowAll /* && mUserName === null */
228  ) {
229  // no oi_timestamp index, so only alphabetical sorting in this case.
230  return $field === 'img_name';
231  }
232 
233  return in_array( $field, $sortable );
234  }
235 
236  public function getQueryInfo() {
237  // Hacky Hacky Hacky - I want to get query info
238  // for two different tables, without reimplementing
239  // the pager class.
240  $qi = $this->getQueryInfoReal( $this->mTableName );
241 
242  return $qi;
243  }
244 
255  protected function getQueryInfoReal( $table ) {
256  $dbr = $this->getDatabase();
257  $prefix = $table === 'oldimage' ? 'oi' : 'img';
258 
259  $tables = [ $table, 'actor' ];
260  $join_conds = [];
261 
262  if ( $table === 'oldimage' ) {
263  $fields = [
264  'img_timestamp' => 'oi_timestamp',
265  'img_name' => 'oi_name',
266  'img_size' => 'oi_size',
267  'top' => $dbr->addQuotes( 'no' )
268  ];
269  $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
270  } else {
271  $fields = [
272  'img_timestamp',
273  'img_name',
274  'img_size',
275  'top' => $dbr->addQuotes( 'yes' )
276  ];
277  $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
278  }
279 
280  $options = [];
281 
282  # Description field
283  $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
284  $tables += $commentQuery['tables'];
285  $fields += $commentQuery['fields'];
286  $join_conds += $commentQuery['joins'];
287  $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
288 
289  # Actor fields
290  $fields[] = 'actor_user';
291  $fields[] = 'actor_name';
292 
293  # Depends on $wgMiserMode
294  # Will also not happen if mShowAll is true.
295  if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
296  $fields['count'] = $dbr->buildSelectSubquery(
297  'oldimage',
298  'COUNT(oi_archive_name)',
299  'oi_name = img_name',
300  __METHOD__
301  );
302  }
303 
304  return [
305  'tables' => $tables,
306  'fields' => $fields,
307  'conds' => $this->buildQueryConds( $table ),
308  'options' => $options,
309  'join_conds' => $join_conds
310  ];
311  }
312 
322  public function reallyDoQuery( $offset, $limit, $order ) {
323  $dbr = $this->getDatabase();
324  $prevTableName = $this->mTableName;
325  $this->mTableName = 'image';
326  list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
327  $this->buildQueryInfo( $offset, $limit, $order );
328  $imageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
329  $this->mTableName = $prevTableName;
330 
331  if ( !$this->mShowAll ) {
332  return $imageRes;
333  }
334 
335  $this->mTableName = 'oldimage';
336 
337  # Hacky...
338  $oldIndex = $this->mIndexField;
339  foreach ( $this->mIndexField as &$index ) {
340  if ( substr( $index, 0, 4 ) !== 'img_' ) {
341  throw new MWException( "Expected to be sorting on an image table field" );
342  }
343  $index = 'oi_' . substr( $index, 4 );
344  }
345 
346  list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
347  $this->buildQueryInfo( $offset, $limit, $order );
348  $oldimageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
349 
350  $this->mTableName = $prevTableName;
351  $this->mIndexField = $oldIndex;
352 
353  return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
354  }
355 
367  protected function combineResult( $res1, $res2, $limit, $order ) {
368  $res1->rewind();
369  $res2->rewind();
370  $topRes1 = $res1->fetchObject();
371  $topRes2 = $res2->fetchObject();
372  $resultArray = [];
373  for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
374  if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
375  if ( $order !== IndexPager::QUERY_ASCENDING ) {
376  $resultArray[] = $topRes1;
377  $topRes1 = $res1->fetchObject();
378  } else {
379  $resultArray[] = $topRes2;
380  $topRes2 = $res2->fetchObject();
381  }
382  } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
383  $resultArray[] = $topRes2;
384  $topRes2 = $res2->fetchObject();
385  } else {
386  $resultArray[] = $topRes1;
387  $topRes1 = $res1->fetchObject();
388  }
389  }
390 
391  for ( ; $i < $limit && $topRes1; $i++ ) {
392  $resultArray[] = $topRes1;
393  $topRes1 = $res1->fetchObject();
394  }
395 
396  for ( ; $i < $limit && $topRes2; $i++ ) {
397  $resultArray[] = $topRes2;
398  $topRes2 = $res2->fetchObject();
399  }
400 
401  return new FakeResultWrapper( $resultArray );
402  }
403 
404  public function getIndexField() {
405  return [ self::INDEX_FIELDS[$this->mSort] ];
406  }
407 
408  public function getDefaultSort() {
409  if ( $this->mShowAll && $this->getConfig()->get( MainConfigNames::MiserMode ) &&
410  $this->mUserName === null ) {
411  // Unfortunately no index on oi_timestamp.
412  return 'img_name';
413  } else {
414  return 'img_timestamp';
415  }
416  }
417 
418  protected function doBatchLookups() {
419  $userIds = [];
420  $this->mResult->seek( 0 );
421  foreach ( $this->mResult as $row ) {
422  if ( $row->actor_user ) {
423  $userIds[] = $row->actor_user;
424  }
425  }
426  # Do a link batch query for names and userpages
427  $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
428  }
429 
436  public function formatValue( $field, $value ) {
437  $linkRenderer = $this->getLinkRenderer();
438  switch ( $field ) {
439  case 'thumb':
440  $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
441  $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
442  // If statement for paranoia
443  if ( $file ) {
444  $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
445  if ( $thumb ) {
446  return $thumb->toHtml( [ 'desc-link' => true ] );
447  } else {
448  return $this->msg( 'thumbnail_error', '' )->escaped();
449  }
450  } else {
451  return htmlspecialchars( $this->getCurrentRow()->img_name );
452  }
453  case 'img_timestamp':
454  // We may want to make this a link to the "old" version when displaying old files
455  return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
456  case 'img_name':
457  static $imgfile = null;
458  if ( $imgfile === null ) {
459  $imgfile = $this->msg( 'imgfile' )->text();
460  }
461 
462  // Weird files can maybe exist? T24227
463  $filePage = Title::makeTitleSafe( NS_FILE, $value );
464  if ( $filePage ) {
465  $link = $linkRenderer->makeKnownLink(
466  $filePage,
467  $filePage->getText()
468  );
469  $download = Xml::element(
470  'a',
471  [ 'href' => $this->localRepo->newFile( $filePage )->getUrl() ],
472  $imgfile
473  );
474  $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
475 
476  // Add delete links if allowed
477  // From https://github.com/Wikia/app/pull/3859
478  if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
479  $deleteMsg = $this->msg( 'listfiles-delete' )->text();
480 
481  $delete = $linkRenderer->makeKnownLink(
482  $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
483  );
484  $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
485 
486  return "$link $download $delete";
487  }
488 
489  return "$link $download";
490  } else {
491  return htmlspecialchars( $value );
492  }
493  case 'img_actor':
494  if ( $this->mCurrentRow->actor_user ) {
495  $name = $this->mCurrentRow->actor_name;
496  $link = $linkRenderer->makeLink(
497  Title::makeTitle( NS_USER, $name ),
498  $name
499  );
500  } else {
501  $link = $value !== null ? htmlspecialchars( $value ) : '';
502  }
503 
504  return $link;
505  case 'img_size':
506  return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
507  case 'img_description':
508  $field = $this->mCurrentRow->description_field;
509  $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
510  return Linker::formatComment( $value );
511  case 'count':
512  return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
513  case 'top':
514  // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
515  return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
516  default:
517  throw new MWException( "Unknown field '$field'" );
518  }
519  }
520 
521  public function getForm() {
522  $formDescriptor = [];
523  $formDescriptor['limit'] = [
524  'type' => 'select',
525  'name' => 'limit',
526  'label-message' => 'table_pager_limit_label',
527  'options' => $this->getLimitSelectList(),
528  'default' => $this->mLimit,
529  ];
530 
531  $formDescriptor['user'] = [
532  'type' => 'user',
533  'name' => 'user',
534  'id' => 'mw-listfiles-user',
535  'label-message' => 'username',
536  'default' => $this->mUserName,
537  'size' => '40',
538  'maxlength' => '255',
539  ];
540 
541  $formDescriptor['ilshowall'] = [
542  'type' => 'check',
543  'name' => 'ilshowall',
544  'id' => 'mw-listfiles-show-all',
545  'label-message' => 'listfiles-show-all',
546  'default' => $this->mShowAll,
547  ];
548 
549  $query = $this->getRequest()->getQueryValues();
550  unset( $query['title'] );
551  unset( $query['limit'] );
552  unset( $query['ilsearch'] );
553  unset( $query['ilshowall'] );
554  unset( $query['user'] );
555 
556  HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
557  ->setMethod( 'get' )
558  ->setId( 'mw-listfiles-form' )
559  ->setTitle( $this->getTitle() )
560  ->setSubmitTextMsg( 'table_pager_limit_submit' )
561  ->setWrapperLegendMsg( 'listfiles' )
562  ->addHiddenFields( $query )
563  ->prepareForm()
564  ->displayForm( '' );
565  }
566 
567  protected function getTableClass() {
568  return parent::getTableClass() . ' listfiles';
569  }
570 
571  protected function getNavClass() {
572  return parent::getNavClass() . ' listfiles_nav';
573  }
574 
575  protected function getSortHeaderClass() {
576  return parent::getSortHeaderClass() . ' listfiles_sort';
577  }
578 
579  public function getPagingQueries() {
580  $queries = parent::getPagingQueries();
581  if ( $this->mUserName !== null ) {
582  # Append the username to the query string
583  foreach ( $queries as &$query ) {
584  if ( $query !== false ) {
585  $query['user'] = $this->mUserName;
586  }
587  }
588  }
589 
590  return $queries;
591  }
592 
593  public function getDefaultQuery() {
594  $queries = parent::getDefaultQuery();
595  if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
596  $queries['user'] = $this->mUserName;
597  }
598 
599  return $queries;
600  }
601 
602  public function getTitle() {
603  return SpecialPage::getTitleFor( 'Listfiles' );
604  }
605 }
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()
Handle database storage of comments such as edit summaries and log reasons.
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:344
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
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.
__construct(IContextSource $context, CommentStore $commentStore, LinkRenderer $linkRenderer, ILoadBalancer $loadBalancer, RepoGroup $repoGroup, UserCache $userCache, UserNameUtils $userNameUtils, $userName, $search, $including, $showAll)
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
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition: Linker.php:1405
MediaWiki exception.
Definition: MWException.php:29
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
Prioritized list of file repositories.
Definition: RepoGroup.php:29
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:342
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:31
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
static newFromName( $name, $validate='valid')
Definition: User.php:598
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:43
Interface for objects which can provide a MediaWiki context on request.
Create and track the database connections and transactions 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