MediaWiki REL1_38
ImageListPager.php
Go to the documentation of this file.
1<?php
27
32
33 protected $mFieldNames = null;
34
35 // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
36 protected $mQueryConds = [];
37
38 protected $mUserName = null;
39
45 protected $mUser = null;
46
47 protected $mIncluding = false;
48
49 protected $mShowAll = false;
50
51 protected $mTableName = 'image';
52
55
57 private $localRepo;
58
60 private $userCache;
61
65 private const INDEX_FIELDS = [
66 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
67 'img_name' => [ 'img_name' ],
68 'img_size' => [ 'img_size', 'img_name' ],
69 ];
70
84 public function __construct(
88 ILoadBalancer $loadBalancer,
89 RepoGroup $repoGroup,
91 UserNameUtils $userNameUtils,
92 $userName,
93 $search,
94 $including,
95 $showAll
96 ) {
97 $this->setContext( $context );
98
99 $this->mIncluding = $including;
100 $this->mShowAll = $showAll;
101 $dbr = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
102
103 if ( $userName !== null && $userName !== '' ) {
104 $nt = Title::makeTitleSafe( NS_USER, $userName );
105 if ( $nt === null ) {
106 $this->outputUserDoesNotExist( $userName );
107 } else {
108 $this->mUserName = $nt->getText();
109 $user = User::newFromName( $this->mUserName, false );
110 if ( $user ) {
111 $this->mUser = $user;
112 }
113 if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
114 $this->outputUserDoesNotExist( $userName );
115 }
116 }
117 }
118
119 if ( !$including ) {
120 if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
121 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
122 } else {
123 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
124 }
125 } else {
126 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
127 }
128 // Set database before parent constructor to avoid setting it there with wfGetDB
129 $this->mDb = $dbr;
130
131 parent::__construct( $context, $linkRenderer );
132 $this->commentStore = $commentStore;
133 $this->localRepo = $repoGroup->getLocalRepo();
134 $this->userCache = $userCache;
135 }
136
142 public function getRelevantUser() {
143 return $this->mUser;
144 }
145
151 protected function outputUserDoesNotExist( $userName ) {
152 $this->getOutput()->addHtml( Html::warningBox(
153 $this->getOutput()->msg( 'listfiles-userdoesnotexist', wfEscapeWikiText( $userName ) )->parse(),
154 'mw-userpage-userdoesnotexist'
155 ) );
156 }
157
165 protected function buildQueryConds( $table ) {
166 $prefix = $table === 'image' ? 'img' : 'oi';
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( '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( '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( '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->next();
371 $topRes2 = $res2->next();
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->next();
378 } else {
379 $resultArray[] = $topRes2;
380 $topRes2 = $res2->next();
381 }
382 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
383 $resultArray[] = $topRes2;
384 $topRes2 = $res2->next();
385 } else {
386 $resultArray[] = $topRes1;
387 $topRes1 = $res1->next();
388 }
389 }
390
391 for ( ; $i < $limit && $topRes1; $i++ ) {
392 $resultArray[] = $topRes1;
393 $topRes1 = $res1->next();
394 }
395
396 for ( ; $i < $limit && $topRes2; $i++ ) {
397 $resultArray[] = $topRes2;
398 $topRes2 = $res2->next();
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( 'MiserMode' ) && $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()
IContextSource $context
setContext(IContextSource $context)
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.
LocalRepo $localRepo
CommentStore $commentStore
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.
const INDEX_FIELDS
The unique sort fields for the sort options for unique paginate.
UserCache $userCache
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)
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
string string[] $mIndexField
The index to actually be used for ordering.
getDatabase()
Get the Database object in use.
LinkRenderer $linkRenderer
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
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:1426
A repository that stores files in the local filesystem and registers them in the wiki's own database.
Definition LocalRepo.php:41
MediaWiki exception.
Class that generates HTML anchor link elements for pages.
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
Prioritized list of file repositories.
Definition RepoGroup.php:32
getLocalRepo()
Get the local repository, i.e.
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.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:68
static newFromName( $name, $validate='valid')
Definition User.php:596
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Database cluster connection, tracking, load balancing, and transaction manager interface.
getConnectionRef( $i, $groups=[], $domain=false, $flags=0)
Get a live database handle reference for a server index.
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