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  /* For reference, the indices we can use for sorting are:
216  * On the image table: img_actor_timestamp, img_size, img_timestamp
217  * On oldimage: oi_actor_timestamp, oi_name_timestamp
218  *
219  * In particular that means we cannot sort by timestamp when not filtering
220  * by user and including old images in the results. Which is sad. (T279982)
221  */
222  if ( $this->getConfig()->get( MainConfigNames::MiserMode ) && $this->mUserName !== null ) {
223  // If we're sorting by user, the index only supports sorting by time.
224  return $field === 'img_timestamp';
225  } elseif ( $this->getConfig()->get( MainConfigNames::MiserMode )
226  && $this->mShowAll /* && mUserName === null */
227  ) {
228  // no oi_timestamp index, so only alphabetical sorting in this case.
229  return $field === 'img_name';
230  }
231 
232  return isset( self::INDEX_FIELDS[$field] );
233  }
234 
235  public function getQueryInfo() {
236  // Hacky Hacky Hacky - I want to get query info
237  // for two different tables, without reimplementing
238  // the pager class.
239  $qi = $this->getQueryInfoReal( $this->mTableName );
240 
241  return $qi;
242  }
243 
254  protected function getQueryInfoReal( $table ) {
255  $dbr = $this->getDatabase();
256  $prefix = $table === 'oldimage' ? 'oi' : 'img';
257 
258  $tables = [ $table, 'actor' ];
259  $join_conds = [];
260 
261  if ( $table === 'oldimage' ) {
262  $fields = [
263  'img_timestamp' => 'oi_timestamp',
264  'img_name' => 'oi_name',
265  'img_size' => 'oi_size',
266  'top' => $dbr->addQuotes( 'no' )
267  ];
268  $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
269  } else {
270  $fields = [
271  'img_timestamp',
272  'img_name',
273  'img_size',
274  'top' => $dbr->addQuotes( 'yes' )
275  ];
276  $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
277  }
278 
279  $options = [];
280 
281  # Description field
282  $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
283  $tables += $commentQuery['tables'];
284  $fields += $commentQuery['fields'];
285  $join_conds += $commentQuery['joins'];
286  $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
287 
288  # Actor fields
289  $fields[] = 'actor_user';
290  $fields[] = 'actor_name';
291 
292  # Depends on $wgMiserMode
293  # Will also not happen if mShowAll is true.
294  if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
295  $fields['count'] = $dbr->buildSelectSubquery(
296  'oldimage',
297  'COUNT(oi_archive_name)',
298  'oi_name = img_name',
299  __METHOD__
300  );
301  }
302 
303  return [
304  'tables' => $tables,
305  'fields' => $fields,
306  'conds' => $this->buildQueryConds( $table ),
307  'options' => $options,
308  'join_conds' => $join_conds
309  ];
310  }
311 
321  public function reallyDoQuery( $offset, $limit, $order ) {
322  $dbr = $this->getDatabase();
323  $prevTableName = $this->mTableName;
324  $this->mTableName = 'image';
325  [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
326  $this->buildQueryInfo( $offset, $limit, $order );
327  $imageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
328  $this->mTableName = $prevTableName;
329 
330  if ( !$this->mShowAll ) {
331  return $imageRes;
332  }
333 
334  $this->mTableName = 'oldimage';
335 
336  # Hacky...
337  $oldIndex = $this->mIndexField;
338  foreach ( $this->mIndexField as &$index ) {
339  if ( substr( $index, 0, 4 ) !== 'img_' ) {
340  throw new MWException( "Expected to be sorting on an image table field" );
341  }
342  $index = 'oi_' . substr( $index, 4 );
343  }
344 
345  [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
346  $this->buildQueryInfo( $offset, $limit, $order );
347  $oldimageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
348 
349  $this->mTableName = $prevTableName;
350  $this->mIndexField = $oldIndex;
351 
352  return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
353  }
354 
366  protected function combineResult( $res1, $res2, $limit, $order ) {
367  $res1->rewind();
368  $res2->rewind();
369  $topRes1 = $res1->fetchObject();
370  $topRes2 = $res2->fetchObject();
371  $resultArray = [];
372  for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
373  if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
374  if ( $order !== IndexPager::QUERY_ASCENDING ) {
375  $resultArray[] = $topRes1;
376  $topRes1 = $res1->fetchObject();
377  } else {
378  $resultArray[] = $topRes2;
379  $topRes2 = $res2->fetchObject();
380  }
381  } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
382  $resultArray[] = $topRes2;
383  $topRes2 = $res2->fetchObject();
384  } else {
385  $resultArray[] = $topRes1;
386  $topRes1 = $res1->fetchObject();
387  }
388  }
389 
390  for ( ; $i < $limit && $topRes1; $i++ ) {
391  $resultArray[] = $topRes1;
392  $topRes1 = $res1->fetchObject();
393  }
394 
395  for ( ; $i < $limit && $topRes2; $i++ ) {
396  $resultArray[] = $topRes2;
397  $topRes2 = $res2->fetchObject();
398  }
399 
400  return new FakeResultWrapper( $resultArray );
401  }
402 
403  public function getIndexField() {
404  return [ self::INDEX_FIELDS[$this->mSort] ];
405  }
406 
407  public function getDefaultSort() {
408  if ( $this->mShowAll && $this->getConfig()->get( MainConfigNames::MiserMode ) &&
409  $this->mUserName === null ) {
410  // Unfortunately no index on oi_timestamp.
411  return 'img_name';
412  } else {
413  return 'img_timestamp';
414  }
415  }
416 
417  protected function doBatchLookups() {
418  $userIds = [];
419  $this->mResult->seek( 0 );
420  foreach ( $this->mResult as $row ) {
421  if ( $row->actor_user ) {
422  $userIds[] = $row->actor_user;
423  }
424  }
425  # Do a link batch query for names and userpages
426  $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
427  }
428 
435  public function formatValue( $field, $value ) {
436  $linkRenderer = $this->getLinkRenderer();
437  switch ( $field ) {
438  case 'thumb':
439  $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
440  $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
441  // If statement for paranoia
442  if ( $file ) {
443  $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
444  if ( $thumb ) {
445  return $thumb->toHtml( [ 'desc-link' => true ] );
446  } else {
447  return $this->msg( 'thumbnail_error', '' )->escaped();
448  }
449  } else {
450  return htmlspecialchars( $this->getCurrentRow()->img_name );
451  }
452  case 'img_timestamp':
453  // We may want to make this a link to the "old" version when displaying old files
454  return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
455  case 'img_name':
456  static $imgfile = null;
457  if ( $imgfile === null ) {
458  $imgfile = $this->msg( 'imgfile' )->text();
459  }
460 
461  // Weird files can maybe exist? T24227
462  $filePage = Title::makeTitleSafe( NS_FILE, $value );
463  if ( $filePage ) {
464  $link = $linkRenderer->makeKnownLink(
465  $filePage,
466  $filePage->getText()
467  );
468  $download = Xml::element(
469  'a',
470  [ 'href' => $this->localRepo->newFile( $filePage )->getUrl() ],
471  $imgfile
472  );
473  $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
474 
475  // Add delete links if allowed
476  // From https://github.com/Wikia/app/pull/3859
477  if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
478  $deleteMsg = $this->msg( 'listfiles-delete' )->text();
479 
480  $delete = $linkRenderer->makeKnownLink(
481  $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
482  );
483  $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
484 
485  return "$link $download $delete";
486  }
487 
488  return "$link $download";
489  } else {
490  return htmlspecialchars( $value );
491  }
492  case 'img_actor':
493  if ( $this->mCurrentRow->actor_user ) {
494  $name = $this->mCurrentRow->actor_name;
495  $link = $linkRenderer->makeLink(
496  Title::makeTitle( NS_USER, $name ),
497  $name
498  );
499  } else {
500  $link = $value !== null ? htmlspecialchars( $value ) : '';
501  }
502 
503  return $link;
504  case 'img_size':
505  return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
506  case 'img_description':
507  $field = $this->mCurrentRow->description_field;
508  $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
509  return Linker::formatComment( $value );
510  case 'count':
511  return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
512  case 'top':
513  // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
514  return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
515  default:
516  throw new MWException( "Unknown field '$field'" );
517  }
518  }
519 
520  public function getForm() {
521  $formDescriptor = [];
522  $formDescriptor['limit'] = [
523  'type' => 'select',
524  'name' => 'limit',
525  'label-message' => 'table_pager_limit_label',
526  'options' => $this->getLimitSelectList(),
527  'default' => $this->mLimit,
528  ];
529 
530  $formDescriptor['user'] = [
531  'type' => 'user',
532  'name' => 'user',
533  'id' => 'mw-listfiles-user',
534  'label-message' => 'username',
535  'default' => $this->mUserName,
536  'size' => '40',
537  'maxlength' => '255',
538  ];
539 
540  $formDescriptor['ilshowall'] = [
541  'type' => 'check',
542  'name' => 'ilshowall',
543  'id' => 'mw-listfiles-show-all',
544  'label-message' => 'listfiles-show-all',
545  'default' => $this->mShowAll,
546  ];
547 
548  $query = $this->getRequest()->getQueryValues();
549  unset( $query['title'] );
550  unset( $query['limit'] );
551  unset( $query['ilsearch'] );
552  unset( $query['ilshowall'] );
553  unset( $query['user'] );
554 
555  HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
556  ->setMethod( 'get' )
557  ->setId( 'mw-listfiles-form' )
558  ->setTitle( $this->getTitle() )
559  ->setSubmitTextMsg( 'table_pager_limit_submit' )
560  ->setWrapperLegendMsg( 'listfiles' )
561  ->addHiddenFields( $query )
562  ->prepareForm()
563  ->displayForm( '' );
564  }
565 
566  protected function getTableClass() {
567  return parent::getTableClass() . ' listfiles';
568  }
569 
570  protected function getNavClass() {
571  return parent::getNavClass() . ' listfiles_nav';
572  }
573 
574  protected function getSortHeaderClass() {
575  return parent::getSortHeaderClass() . ' listfiles_sort';
576  }
577 
578  public function getPagingQueries() {
579  $queries = parent::getPagingQueries();
580  if ( $this->mUserName !== null ) {
581  # Append the username to the query string
582  foreach ( $queries as &$query ) {
583  if ( $query !== false ) {
584  $query['user'] = $this->mUserName;
585  }
586  }
587  }
588 
589  return $queries;
590  }
591 
592  public function getDefaultQuery() {
593  $queries = parent::getDefaultQuery();
594  if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
595  $queries['user'] = $this->mUserName;
596  }
597 
598  return $queries;
599  }
600 
601  public function getTitle() {
602  return SpecialPage::getTitleFor( 'Listfiles' );
603  }
604 }
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:349
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:79
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
Definition: IndexPager.php:475
string string[] $mIndexField
The index to actually be used for ordering.
Definition: IndexPager.php:111
getDatabase()
Get the Database object in use.
Definition: IndexPager.php:249
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:81
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
Definition: IndexPager.php:84
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:1393
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:29
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:667
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:641
static newFromName( $name, $validate='valid')
Definition: User.php:587
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