MediaWiki  master
ImageListPager.php
Go to the documentation of this file.
1 <?php
29 
30 class ImageListPager extends TablePager {
31 
32  protected $mFieldNames = null;
33 
34  // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
35  protected $mQueryConds = [];
36 
37  protected $mUserName = null;
38 
44  protected $mUser = null;
45 
46  protected $mSearch = '';
47 
48  protected $mIncluding = false;
49 
50  protected $mShowAll = false;
51 
52  protected $mTableName = 'image';
53 
54  public function __construct( IContextSource $context, $userName, $search,
55  $including, $showAll, LinkRenderer $linkRenderer
56  ) {
57  $this->setContext( $context );
58 
59  $this->mIncluding = $including;
60  $this->mShowAll = $showAll;
61 
62  if ( $userName !== null && $userName !== '' ) {
63  $nt = Title::makeTitleSafe( NS_USER, $userName );
64  if ( is_null( $nt ) ) {
65  $this->outputUserDoesNotExist( $userName );
66  } else {
67  $this->mUserName = $nt->getText();
68  $user = User::newFromName( $this->mUserName, false );
69  if ( $user ) {
70  $this->mUser = $user;
71  }
72  if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) {
73  $this->outputUserDoesNotExist( $userName );
74  }
75  }
76  }
77 
78  if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) {
79  $this->mSearch = $search;
80  $nt = Title::newFromText( $this->mSearch );
81 
82  if ( $nt ) {
83  $dbr = wfGetDB( DB_REPLICA );
84  $this->mQueryConds[] = 'LOWER(img_name)' .
85  $dbr->buildLike( $dbr->anyString(),
86  strtolower( $nt->getDBkey() ), $dbr->anyString() );
87  }
88  }
89 
90  if ( !$including ) {
91  if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
92  $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
93  } else {
94  $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
95  }
96  } else {
97  $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
98  }
99 
100  parent::__construct( $context, $linkRenderer );
101  }
102 
108  function getRelevantUser() {
109  return $this->mUser;
110  }
111 
117  protected function outputUserDoesNotExist( $userName ) {
118  $this->getOutput()->wrapWikiMsg(
119  "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
120  [
121  'listfiles-userdoesnotexist',
122  wfEscapeWikiText( $userName ),
123  ]
124  );
125  }
126 
134  protected function buildQueryConds( $table ) {
135  $prefix = $table === 'image' ? 'img' : 'oi';
136  $conds = [];
137 
138  if ( !is_null( $this->mUserName ) ) {
139  // getQueryInfoReal() should have handled the tables and joins.
140  $dbr = wfGetDB( DB_REPLICA );
141  $actorWhere = ActorMigration::newMigration()->getWhere(
142  $dbr,
143  $prefix . '_user',
144  User::newFromName( $this->mUserName, false ),
145  // oldimage doesn't have an index on oi_user, while image does. Set $useId accordingly.
146  $prefix === 'img'
147  );
148  $conds[] = $actorWhere['conds'];
149  }
150 
151  if ( $this->mSearch !== '' ) {
152  $nt = Title::newFromText( $this->mSearch );
153  if ( $nt ) {
154  $dbr = wfGetDB( DB_REPLICA );
155  $conds[] = 'LOWER(' . $prefix . '_name)' .
156  $dbr->buildLike( $dbr->anyString(),
157  strtolower( $nt->getDBkey() ), $dbr->anyString() );
158  }
159  }
160 
161  if ( $table === 'oldimage' ) {
162  // Don't want to deal with revdel.
163  // Future fixme: Show partial information as appropriate.
164  // Would have to be careful about filtering by username when username is deleted.
165  $conds['oi_deleted'] = 0;
166  }
167 
168  // Add mQueryConds in case anyone was subclassing and using the old variable.
169  return $conds + $this->mQueryConds;
170  }
171 
178  function getFieldNames() {
179  if ( !$this->mFieldNames ) {
180  $this->mFieldNames = [
181  'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
182  'img_name' => $this->msg( 'listfiles_name' )->text(),
183  'thumb' => $this->msg( 'listfiles_thumb' )->text(),
184  'img_size' => $this->msg( 'listfiles_size' )->text(),
185  ];
186  if ( is_null( $this->mUserName ) ) {
187  // Do not show username if filtering by username
188  $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text();
189  }
190  // img_description down here, in order so that its still after the username field.
191  $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
192 
193  if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) {
194  $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
195  }
196  if ( $this->mShowAll ) {
197  $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
198  }
199  }
200 
201  return $this->mFieldNames;
202  }
203 
204  function isFieldSortable( $field ) {
205  if ( $this->mIncluding ) {
206  return false;
207  }
208  $sortable = [ 'img_timestamp', 'img_name', 'img_size' ];
209  /* For reference, the indicies we can use for sorting are:
210  * On the image table: img_user_timestamp/img_usertext_timestamp/img_actor_timestamp,
211  * img_size, img_timestamp
212  * On oldimage: oi_usertext_timestamp/oi_actor_timestamp, oi_name_timestamp
213  *
214  * In particular that means we cannot sort by timestamp when not filtering
215  * by user and including old images in the results. Which is sad.
216  */
217  if ( $this->getConfig()->get( 'MiserMode' ) && !is_null( $this->mUserName ) ) {
218  // If we're sorting by user, the index only supports sorting by time.
219  if ( $field === 'img_timestamp' ) {
220  return true;
221  } else {
222  return false;
223  }
224  } elseif ( $this->getConfig()->get( 'MiserMode' )
225  && $this->mShowAll /* && mUserName === null */
226  ) {
227  // no oi_timestamp index, so only alphabetical sorting in this case.
228  if ( $field === 'img_name' ) {
229  return true;
230  } else {
231  return false;
232  }
233  }
234 
235  return in_array( $field, $sortable );
236  }
237 
238  function getQueryInfo() {
239  // Hacky Hacky Hacky - I want to get query info
240  // for two different tables, without reimplementing
241  // the pager class.
242  $qi = $this->getQueryInfoReal( $this->mTableName );
243 
244  return $qi;
245  }
246 
257  protected function getQueryInfoReal( $table ) {
258  $dbr = wfGetDB( DB_REPLICA );
259  $prefix = $table === 'oldimage' ? 'oi' : 'img';
260 
261  $tables = [ $table ];
262  $fields = array_keys( $this->getFieldNames() );
263  $fields = array_combine( $fields, $fields );
264  unset( $fields['img_description'] );
265  unset( $fields['img_user_text'] );
266 
267  if ( $table === 'oldimage' ) {
268  foreach ( $fields as $id => $field ) {
269  if ( substr( $id, 0, 4 ) === 'img_' ) {
270  $fields[$id] = $prefix . substr( $field, 3 );
271  }
272  }
273  $fields['top'] = $dbr->addQuotes( 'no' );
274  } elseif ( $this->mShowAll ) {
275  $fields['top'] = $dbr->addQuotes( 'yes' );
276  }
277  $fields['thumb'] = $prefix . '_name';
278 
279  $options = $join_conds = [];
280 
281  # Description field
282  $commentQuery = CommentStore::getStore()->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  # User fields
289  $actorQuery = ActorMigration::newMigration()->getJoin( $prefix . '_user' );
290  $tables += $actorQuery['tables'];
291  $join_conds += $actorQuery['joins'];
292  $fields['img_user'] = $actorQuery['fields'][$prefix . '_user'];
293  $fields['img_user_text'] = $actorQuery['fields'][$prefix . '_user_text'];
294  $fields['img_actor'] = $actorQuery['fields'][$prefix . '_actor'];
295 
296  # Depends on $wgMiserMode
297  # Will also not happen if mShowAll is true.
298  if ( isset( $fields['count'] ) ) {
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' => $options,
312  'join_conds' => $join_conds
313  ];
314  }
315 
328  function reallyDoQuery( $offset, $limit, $order ) {
329  $prevTableName = $this->mTableName;
330  $this->mTableName = 'image';
331  list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
332  $this->buildQueryInfo( $offset, $limit, $order );
333  $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
334  $this->mTableName = $prevTableName;
335 
336  if ( !$this->mShowAll ) {
337  return $imageRes;
338  }
339 
340  $this->mTableName = 'oldimage';
341 
342  # Hacky...
343  $oldIndex = $this->mIndexField;
344  if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) {
345  throw new MWException( "Expected to be sorting on an image table field" );
346  }
347  $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 );
348 
349  list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
350  $this->buildQueryInfo( $offset, $limit, $order );
351  $oldimageRes = $this->mDb->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, $ascending ) {
371  $res1->rewind();
372  $res2->rewind();
373  $topRes1 = $res1->next();
374  $topRes2 = $res2->next();
375  $resultArray = [];
376  for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
377  if ( strcmp( $topRes1->{$this->mIndexField}, $topRes2->{$this->mIndexField} ) > 0 ) {
378  if ( !$ascending ) {
379  $resultArray[] = $topRes1;
380  $topRes1 = $res1->next();
381  } else {
382  $resultArray[] = $topRes2;
383  $topRes2 = $res2->next();
384  }
385  } elseif ( !$ascending ) {
386  $resultArray[] = $topRes2;
387  $topRes2 = $res2->next();
388  } else {
389  $resultArray[] = $topRes1;
390  $topRes1 = $res1->next();
391  }
392  }
393 
394  for ( ; $i < $limit && $topRes1; $i++ ) {
395  $resultArray[] = $topRes1;
396  $topRes1 = $res1->next();
397  }
398 
399  for ( ; $i < $limit && $topRes2; $i++ ) {
400  $resultArray[] = $topRes2;
401  $topRes2 = $res2->next();
402  }
403 
404  return new FakeResultWrapper( $resultArray );
405  }
406 
407  function getDefaultSort() {
408  if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && is_null( $this->mUserName ) ) {
409  // Unfortunately no index on oi_timestamp.
410  return 'img_name';
411  } else {
412  return 'img_timestamp';
413  }
414  }
415 
416  protected function doBatchLookups() {
417  $userIds = [];
418  $this->mResult->seek( 0 );
419  foreach ( $this->mResult as $row ) {
420  $userIds[] = $row->img_user;
421  }
422  # Do a link batch query for names and userpages
423  UserCache::singleton()->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
424  }
425 
440  function formatValue( $field, $value ) {
441  $services = MediaWikiServices::getInstance();
442  $linkRenderer = $this->getLinkRenderer();
443  switch ( $field ) {
444  case 'thumb':
445  $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
446  $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt );
447  // If statement for paranoia
448  if ( $file ) {
449  $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
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( $value );
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  $link = $linkRenderer->makeKnownLink(
471  $filePage,
472  $filePage->getText()
473  );
474  $download = Xml::element(
475  'a',
476  [ 'href' => $services->getRepoGroup()->getLocalRepo()->newFile( $filePage )->getUrl() ],
477  $imgfile
478  );
479  $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
480 
481  // Add delete links if allowed
482  // From https://github.com/Wikia/app/pull/3859
483  $permissionManager = $services->getPermissionManager();
484 
485  if ( $permissionManager->userCan( 'delete', $this->getUser(), $filePage ) ) {
486  $deleteMsg = $this->msg( 'listfiles-delete' )->text();
487 
488  $delete = $linkRenderer->makeKnownLink(
489  $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
490  );
491  $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
492 
493  return "$link $download $delete";
494  }
495 
496  return "$link $download";
497  } else {
498  return htmlspecialchars( $value );
499  }
500  case 'img_user_text':
501  if ( $this->mCurrentRow->img_user ) {
502  $name = User::whoIs( $this->mCurrentRow->img_user );
503  $link = $linkRenderer->makeLink(
504  Title::makeTitle( NS_USER, $name ),
505  $name
506  );
507  } else {
508  $link = htmlspecialchars( $value );
509  }
510 
511  return $link;
512  case 'img_size':
513  return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
514  case 'img_description':
515  $field = $this->mCurrentRow->description_field;
516  $value = CommentStore::getStore()->getComment( $field, $this->mCurrentRow )->text;
517  return Linker::formatComment( $value );
518  case 'count':
519  return $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 
528  function getForm() {
529  $formDescriptor = [];
530  $formDescriptor['limit'] = [
531  'type' => 'select',
532  'name' => 'limit',
533  'label-message' => 'table_pager_limit_label',
534  'options' => $this->getLimitSelectList(),
535  'default' => $this->mLimit,
536  ];
537 
538  if ( !$this->getConfig()->get( 'MiserMode' ) ) {
539  $formDescriptor['ilsearch'] = [
540  'type' => 'text',
541  'name' => 'ilsearch',
542  'id' => 'mw-ilsearch',
543  'label-message' => 'listfiles_search_for',
544  'default' => $this->mSearch,
545  'size' => '40',
546  'maxlength' => '255',
547  ];
548  }
549 
550  $formDescriptor['user'] = [
551  'type' => 'user',
552  'name' => 'user',
553  'id' => 'mw-listfiles-user',
554  'label-message' => 'username',
555  'default' => $this->mUserName,
556  'size' => '40',
557  'maxlength' => '255',
558  ];
559 
560  $formDescriptor['ilshowall'] = [
561  'type' => 'check',
562  'name' => 'ilshowall',
563  'id' => 'mw-listfiles-show-all',
564  'label-message' => 'listfiles-show-all',
565  'default' => $this->mShowAll,
566  ];
567 
568  $query = $this->getRequest()->getQueryValues();
569  unset( $query['title'] );
570  unset( $query['limit'] );
571  unset( $query['ilsearch'] );
572  unset( $query['ilshowall'] );
573  unset( $query['user'] );
574 
575  $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
576  $htmlForm
577  ->setMethod( 'get' )
578  ->setId( 'mw-listfiles-form' )
579  ->setTitle( $this->getTitle() )
580  ->setSubmitTextMsg( 'table_pager_limit_submit' )
581  ->setWrapperLegendMsg( 'listfiles' )
582  ->addHiddenFields( $query )
583  ->prepareForm()
584  ->displayForm( '' );
585  }
586 
587  protected function getTableClass() {
588  return parent::getTableClass() . ' listfiles';
589  }
590 
591  protected function getNavClass() {
592  return parent::getNavClass() . ' listfiles_nav';
593  }
594 
595  protected function getSortHeaderClass() {
596  return parent::getSortHeaderClass() . ' listfiles_sort';
597  }
598 
599  function getPagingQueries() {
600  $queries = parent::getPagingQueries();
601  if ( !is_null( $this->mUserName ) ) {
602  # Append the username to the query string
603  foreach ( $queries as &$query ) {
604  if ( $query !== false ) {
605  $query['user'] = $this->mUserName;
606  }
607  }
608  }
609 
610  return $queries;
611  }
612 
613  function getDefaultQuery() {
614  $queries = parent::getDefaultQuery();
615  if ( !isset( $queries['user'] ) && !is_null( $this->mUserName ) ) {
616  $queries['user'] = $this->mUserName;
617  }
618 
619  return $queries;
620  }
621 
622  function getTitle() {
623  return SpecialPage::getTitleFor( 'Listfiles' );
624  }
625 }
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:76
setContext(IContextSource $context)
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
Table-based display with a user-selectable sort order.
Definition: TablePager.php:30
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition: User.php:865
outputUserDoesNotExist( $userName)
Add a message to the output stating that the user doesn&#39;t exist.
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:947
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
Definition: IndexPager.php:438
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
string $mIndexField
The index to actually be used for ordering.
Definition: IndexPager.php:105
LinkRenderer $linkRenderer
Definition: IndexPager.php:162
IContextSource $context
getRelevantUser()
Get the user relevant to the ImageList.
getLimitSelectList()
Get a list of items to show in a "<select>" element of limits.
Definition: TablePager.php:367
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:307
Class that generates HTML links for pages.
static newMigration()
Static constructor.
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:1165
isFieldSortable( $field)
static singleton()
Definition: RepoGroup.php:60
__construct(IContextSource $context, $userName, $search, $including, $showAll, LinkRenderer $linkRenderer)
getContext()
Get the base IContextSource object.
makeKnownLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
getQueryInfoReal( $table)
Actually get the query info.
reallyDoQuery( $offset, $limit, $order)
Override reallyDoQuery to mix together two queries.
const NS_FILE
Definition: Defines.php:66
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:610
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
static getStore()
combineResult( $res1, $res2, $limit, $ascending)
Combine results from 2 tables.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Overloads the relevant methods of the real ResultsWrapper so it doesn&#39;t go anywhere near an actual da...
getFieldNames()
The array keys (but not the array values) are used in sql.
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
makeLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
formatValue( $field, $value)
const DB_REPLICA
Definition: defines.php:25
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
User null $mUser
The relevant user.
buildQueryConds( $table)
Build the where clause of the query.
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Definition: IndexPager.php:74
static singleton()
Definition: UserCache.php:34
int $mLimit
The maximum number of entries to show.
Definition: IndexPager.php:92
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:317